Skip to content

Commit

Permalink
Excel before 1900 and AWS signed requests. (#11373)
Browse files Browse the repository at this point in the history
  • Loading branch information
jdunkerley authored Oct 28, 2024
1 parent 5bf064f commit 78d9e34
Show file tree
Hide file tree
Showing 19 changed files with 785 additions and 134 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,10 +28,12 @@
- [The user may set description and labels of an Enso Cloud asset
programmatically.][11255]
- [DB_Table may be saved as a Data Link.][11371]
- [Support for dates before 1900 in Excel and signed AWS requests.][11373]

[11235]: https://github.com/enso-org/enso/pull/11235
[11255]: https://github.com/enso-org/enso/pull/11255
[11371]: https://github.com/enso-org/enso/pull/11371
[11373]: https://github.com/enso-org/enso/pull/11373

#### Enso Language & Runtime

Expand Down
114 changes: 114 additions & 0 deletions distribution/lib/Standard/AWS/0.0.0-dev/src/AWS.enso
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
from Standard.Base import all
import Standard.Base.Errors.Common.Missing_Argument
import Standard.Base.Network.HTTP.HTTP_Error.HTTP_Error
import Standard.Base.Network.HTTP.Request.Request
import Standard.Base.Network.HTTP.Request_Body.Request_Body
import Standard.Base.Network.HTTP.Request_Error
import Standard.Base.Network.HTTP.Response.Response
from Standard.Base.Metadata.Widget import Text_Input
from Standard.Base.Network.HTTP import if_fetch_method, if_post_method, internal_http_client, with_hash_and_client

import project.AWS_Credential.AWS_Credential
import project.AWS_Region.AWS_Region
import project.Errors.Invalid_AWS_URI

polyglot java import org.enso.aws.ClientBuilder

## Methods for interacting with AWS services.
type AWS
## ALIAS download, http get
ICON data_input
Fetches from an AWS URI signing the request with the necessary headers,
and returns the response, parsing the body if the content-type is
recognised. Returns an error if the status code does not represent a
successful response.

Arguments:
- method: The HTTP method to use. Must be one of `HTTP_Method.Get`,
`HTTP_Method.Head`, `HTTP_Method.Delete`, `HTTP_Method.Options`.
Defaults to `HTTP_Method.Get`.
- headers: The headers to send with the request. Defaults to an empty
vector.
- format: The format to use for interpreting the response.
Defaults to `Auto_Detect`. If `Raw_Response` is selected or if the
format cannot be determined automatically, a raw HTTP `Response` will
be returned.
- credentials: The credentials to use for signing the request. Defaults
to the default AWS credentials.
- region_service: The region and service to use for signing the request.
Defaults to the region and service parsed from the URI.
@uri (Text_Input display=..Always)
@format File_Format.default_widget
@headers Header.default_widget
@credentials AWS_Credential.default_widget
signed_fetch : URI -> HTTP_Method -> (Vector (Header | Pair Text Text)) -> File_Format -> AWS_Credential -> AWS_Region_Service -> Any
signed_fetch (uri:URI=(Missing_Argument.throw "uri")) (method:HTTP_Method=..Get) (headers:(Vector (Header | Pair Text Text))=[]) (format = Auto_Detect) credentials:AWS_Credential=..Default (region_service:AWS_Region_Service=(AWS.resolve_region_and_service uri)) = if_fetch_method method <|
request = Request.new method uri (Header.unify_vector headers) Request_Body.Empty
http = with_hash_and_client HTTP.new hash_method=AWS.hash_bytes make_client=(_make_client credentials region_service)
raw_response = http.request request
raw_response.decode format=format if_unsupported=raw_response.with_materialized_body

## ALIAS http post, upload
ICON data_upload
Writes the provided data to the provided AWS URI signing the request with
the necessary headers. Returns the response, parsing the body if the
content-type is recognised. Returns an error if the status code does not
represent a successful response.

Arguments:
- uri: The URI to fetch.
- body: The data to write. See `Supported Body Types` below.
- method: The HTTP method to use. Must be one of `HTTP_Method.Post`,
`HTTP_Method.Put`, `HTTP_Method.Patch`. Defaults to `HTTP_Method.Post`.
- headers: The headers to send with the request. Defaults to an empty
vector.
- response_format: The format to use for interpreting the response.
Defaults to `Auto_Detect`. If `Raw_Response` is selected or if the
format cannot be determined automatically, a raw HTTP `Response` will
be returned.
- credentials: The credentials to use for signing the request. Defaults
to the default AWS credentials.
- region_service: The region and service to use for signing the request.
Defaults to the region and service parsed from the URI.
@uri (Text_Input display=..Always)
@format File_Format.default_widget
@headers Header.default_widget
@credentials AWS_Credential.default_widget
signed_post : (URI | Text) -> Request_Body -> HTTP_Method -> Vector (Header | Pair Text Text) -> Response ! Request_Error | HTTP_Error
signed_post (uri:URI=(Missing_Argument.throw "uri")) (body:Request_Body=..Empty) (method:HTTP_Method=..Post) (headers:(Vector (Header | Pair Text Text))=[]) (response_format = Auto_Detect) credentials:AWS_Credential=..Default (region_service:AWS_Region_Service=(AWS.resolve_region_and_service uri)) = if_post_method method <|
request = Request.new method uri (Header.unify_vector headers) body
http = with_hash_and_client HTTP.new hash_method=AWS.hash_bytes make_client=(_make_client credentials region_service)
raw_response = http.request request
raw_response.decode format=response_format if_unsupported=raw_response.with_materialized_body

## PRIVATE
Hash a Vector of bytes using SHA256 (as used by AWS).
hash_bytes : Vector Integer -> Text
hash_bytes bytes:Vector = ClientBuilder.getSHA256 bytes

## Resolve the region and service from an AWS based URI.
Splits a standard form AWS URI into the region and service.

The URI must be in the forms:
- `https://(*.)<service>.<region>.amazonaws.com`.
- `https://(*.)<region>.<service>.amazonaws.com`.

Arguments:
- uri: The URI to resolve.
resolve_region_and_service : URI -> AWS_Region_Service
resolve_region_and_service (uri:URI=(Missing_Argument.throw "uri")) =
region_regex = regex "^(([a-z]{2}-[^.]+?-\d+)|(global))$"
domain = uri.host.split '.'
if (domain.length < 4 || (domain.at -1) != "com" || (domain.at -2) != "amazonaws") then Error.throw (Invalid_AWS_URI.Error domain.length.to_text+":"+uri.to_text) else
if (domain.at -3).match region_regex then AWS_Region_Service.Region_Service region=(domain.at -3) service=(domain.at -4) else
if (domain.at -4).match region_regex then AWS_Region_Service.Region_Service region=(domain.at -4) service=(domain.at -3) else
Error.throw (Invalid_AWS_URI.Error domain.to_display_text)

## Holds the region and service of an AWS URI.
type AWS_Region_Service
## Holds the region and service of an AWS URI.
Region_Service region:Text service:Text

private _make_client credentials region_service http hash =
builder = ClientBuilder.new credentials.as_java (AWS_Region.Region region_service.region).as_java
builder.createSignedClient region_service.region region_service.service (internal_http_client http "") hash
9 changes: 9 additions & 0 deletions distribution/lib/Standard/AWS/0.0.0-dev/src/Errors.enso
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@ from Standard.Base import all

polyglot java import software.amazon.awssdk.core.exception.SdkClientException

## An invalid URI was provided.
type Invalid_AWS_URI
## PRIVATE
Error uri:Text

## PRIVATE
to_display_text : Text
to_display_text self = "Invalid AWS URI: " + self.uri

## An error in the core AWS SDK
type AWS_SDK_Error
## PRIVATE
Expand Down
1 change: 1 addition & 0 deletions distribution/lib/Standard/AWS/0.0.0-dev/src/Main.enso
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export project.AWS.AWS
export project.AWS_Credential.AWS_Credential
export project.AWS_Region.AWS_Region
export project.Database.Redshift.Redshift_Details.Redshift_Details
Expand Down
6 changes: 3 additions & 3 deletions distribution/lib/Standard/Base/0.0.0-dev/src/Data.enso
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ list (directory:(Text | File)=enso_project.root) (name_filter:Text="") recursive
import Standard.Base.Data
file = enso_project.data / "spreadsheet.xls"
Data.fetch URL . body . write file
@uri Text_Input
@uri (Text_Input display=..Always)
@format Data_Read_Helpers.format_widget_with_raw_response
@headers Header.default_widget
fetch : (URI | Text) -> HTTP_Method -> Vector (Header | Pair Text Text) -> File_Format -> Any ! Request_Error | HTTP_Error
Expand Down Expand Up @@ -322,7 +322,7 @@ fetch (uri:(URI | Text)=(Missing_Argument.throw "uri")) (method:HTTP_Method=..Ge
test_file = enso_project.data / "sample.txt"
form_data = Dictionary.from_vector [["key", "val"], ["a_file", test_file]]
response = Data.post url_post (Request_Body.Form_Data form_data url_encoded=True)
@uri Text_Input
@uri (Text_Input display=..Always)
@headers Header.default_widget
@response_format Data_Read_Helpers.format_widget_with_raw_response
post : (URI | Text) -> Request_Body -> HTTP_Method -> Vector (Header | Pair Text Text) -> File_Format -> Any ! Request_Error | HTTP_Error
Expand All @@ -342,7 +342,7 @@ post (uri:(URI | Text)=(Missing_Argument.throw "uri")) (body:Request_Body=..Empt
`HTTP_Method.Head`, `HTTP_Method.Delete`, `HTTP_Method.Options`.
Defaults to `HTTP_Method.Get`.
- headers: The headers to send with the request. Defaults to an empty vector.
@uri Text_Input
@uri (Text_Input display=..Always)
@headers Header.default_widget
download : (URI | Text) -> Writable_File -> HTTP_Method -> Vector (Header | Pair Text Text) -> File ! Request_Error | HTTP_Error
download (uri:(URI | Text)=(Missing_Argument.throw "uri")) file:Writable_File (method:HTTP_Method=..Get) (headers:(Vector (Header | Pair Text Text))=[]) =
Expand Down
Loading

0 comments on commit 78d9e34

Please sign in to comment.