diff --git a/CHANGELOG.md b/CHANGELOG.md index 1311a0d49b..4ffc657ca3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -592,6 +592,7 @@ - [Added `Previous_Value` option to `fill_nothing` and `fill_empty`.][8105] - [Added `Table.format` for the in-memory backend.][8150] - [Implemented truncate `Date_Time` for database backend (Postgres only).][8235] +- [Initial Enso Cloud APIs.][8006] [debug-shortcuts]: https://github.com/enso-org/enso/blob/develop/app/gui/docs/product/shortcuts.md#debug @@ -841,6 +842,7 @@ [7947]: https://github.com/enso-org/enso/pull/7947 [7979]: https://github.com/enso-org/enso/pull/7979 [8005]: https://github.com/enso-org/enso/pull/8005 +[8006]: https://github.com/enso-org/enso/pull/8006 [8029]: https://github.com/enso-org/enso/pull/8029 [8083]: https://github.com/enso-org/enso/pull/8083 [8105]: https://github.com/enso-org/enso/pull/8105 diff --git a/distribution/lib/Standard/AWS/0.0.0-dev/src/S3/S3.enso b/distribution/lib/Standard/AWS/0.0.0-dev/src/S3/S3.enso index f889ebf867..e438320d67 100644 --- a/distribution/lib/Standard/AWS/0.0.0-dev/src/S3/S3.enso +++ b/distribution/lib/Standard/AWS/0.0.0-dev/src/S3/S3.enso @@ -46,7 +46,8 @@ list_objects : Text -> Text -> AWS_Credential | Nothing -> Integer -> Vector Tex list_objects bucket prefix="" credentials:(AWS_Credential | Nothing)=Nothing max_count=1000 = read_bucket bucket prefix credentials delimiter="" max_count=max_count . second -## Reads an S3 bucket returning a pair of vectors, one with common prefixes and +## PRIVATE + Reads an S3 bucket returning a pair of vectors, one with common prefixes and one with object keys. @credentials AWS_Credential.default_widget read_bucket : Text -> Text -> AWS_Credential | Nothing -> Integer -> Text -> Pair Vector Vector ! S3_Error @@ -73,7 +74,8 @@ read_bucket bucket prefix="" credentials:(AWS_Credential | Nothing)=Nothing deli iterator request 0 [] [] True -## Gets the metadata of a bucket or object. +## ADVANCED + Gets the metadata of a bucket or object. Arguments: - bucket: the name of the bucket. diff --git a/distribution/lib/Standard/AWS/0.0.0-dev/src/S3/S3_File.enso b/distribution/lib/Standard/AWS/0.0.0-dev/src/S3/S3_File.enso index 0b8d28a49e..7c063a8806 100644 --- a/distribution/lib/Standard/AWS/0.0.0-dev/src/S3/S3_File.enso +++ b/distribution/lib/Standard/AWS/0.0.0-dev/src/S3/S3_File.enso @@ -1,7 +1,9 @@ from Standard.Base import all import Standard.Base.Errors.Common.Syntax_Error import Standard.Base.Errors.File_Error.File_Error +import Standard.Base.Errors.Illegal_Argument.Illegal_Argument import Standard.Base.Errors.Unimplemented.Unimplemented +import Standard.Base.System.File_Format.File_For_Read import Standard.Base.System.Input_Stream.Input_Stream import Standard.Base.System.Output_Stream.Output_Stream @@ -12,7 +14,13 @@ import project.S3.S3 ## Represents an S3 file or folder If the path ends with a slash, it is a folder. Otherwise, it is a file. type S3_File - ## PUBLIC + ## Given an S3 URI create a file representation. + + Arguments: + - uri: The URI of the file. + The URI must be in the form `s3://bucket/path/to/file`. + - credentials: The credentials to use when accessing the file. + If not specified, the default credentials are used. new : Text -> AWS_Credential | Nothing -> S3_File new uri="s3://" credentials=Nothing = parts = S3.parse_uri uri @@ -77,8 +85,8 @@ type S3_File The created stream is automatically closed when `action` returns (even if it returns exceptionally). - with_input_stream : Vector File_Access -> (Input_Stream -> Any ! File_Error) -> Any ! File_Error - with_input_stream self open_options action = + with_input_stream : Vector File_Access -> (Input_Stream -> Any ! File_Error) -> Any ! S3_Error | Illegal_Argument + with_input_stream self open_options action = if self.is_directory then Error.throw (Illegal_Argument.Error "S3 folders cannot be opened as a stream." self.uri) else if (open_options != [File_Access.Read]) then Error.throw (S3_Error.Error "S3 files can only be opened for reading." self.uri) else response_body = S3.get_object self.bucket self.prefix self.credentials response_body.with_stream action @@ -206,3 +214,6 @@ type S3_File sub_folders = pair.first . map key-> S3_File.Value self.bucket key self.credentials files = pair.second . map key-> S3_File.Value self.bucket key self.credentials sub_folders + files + +## PRIVATE +File_For_Read.from (that:S3_File) = File_For_Read.Value that.uri that.name that.extension diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data.enso index f0b275f1be..53c21fea15 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Data.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data.enso @@ -15,6 +15,7 @@ import project.Network.HTTP.Request.Request import project.Network.HTTP.Request_Body.Request_Body import project.Network.HTTP.Request_Error import project.Network.URI.URI +import project.Network.URI_With_Query.URI_With_Query import project.Nothing.Nothing import project.System.File.File from project.Data.Boolean import Boolean, False, True @@ -173,8 +174,8 @@ list_directory directory name_filter=Nothing recursive=False = import Standard.Base.Data file = enso_project.data / "spreadsheet.xls" Data.fetch URL . body . to_file file -fetch : (URI | Text) -> HTTP_Method -> Vector (Header | Pair Text Text) -> Boolean -> Any -fetch (uri:(URI | Text)) (method:HTTP_Method=HTTP_Method.Get) (headers:(Vector (Header | Pair Text Text))=[]) (try_auto_parse_response:Boolean=True) = +fetch : (URI | URI_With_Query | Text) -> HTTP_Method -> Vector (Header | Pair Text Text) -> Boolean -> Any +fetch (uri:(URI | URI_With_Query | Text)) (method:HTTP_Method=HTTP_Method.Get) (headers:(Vector (Header | Pair Text Text))=[]) (try_auto_parse_response:Boolean=True) = response = HTTP.fetch uri method headers if try_auto_parse_response.not then response.with_materialized_body else response.decode if_unsupported=response.with_materialized_body @@ -297,8 +298,8 @@ fetch (uri:(URI | Text)) (method:HTTP_Method=HTTP_Method.Get) (headers:(Vector ( test_file = enso_project.data / "sample.txt" form_data = Map.from_vector [["key", "val"], ["a_file", test_file]] response = Data.post url_post (Request_Body.Form_Data form_data url_encoded=True) -post : (URI | Text) -> Request_Body -> HTTP_Method -> Vector (Header | Pair Text Text) -> Boolean -> Any -post (uri:(URI | Text)) (body:Request_Body=Request_Body.Empty) (method:HTTP_Method=HTTP_Method.Post) (headers:(Vector (Header | Pair Text Text))=[]) (try_auto_parse_response:Boolean=True) = +post : (URI | URI_With_Query | Text) -> Request_Body -> HTTP_Method -> Vector (Header | Pair Text Text) -> Boolean -> Any +post (uri:(URI | URI_With_Query | Text)) (body:Request_Body=Request_Body.Empty) (method:HTTP_Method=HTTP_Method.Post) (headers:(Vector (Header | Pair Text Text))=[]) (try_auto_parse_response:Boolean=True) = response = HTTP.post uri body method headers if try_auto_parse_response.not then response.with_materialized_body else response.decode if_unsupported=response.with_materialized_body diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Array.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Array.enso index 55117c012b..f8cd7fa841 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Array.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Array.enso @@ -144,7 +144,7 @@ type Array sort self (order = Sort_Direction.Ascending) on=Nothing by=Nothing on_incomparable=Problem_Behavior.Ignore = Vector.sort self order on by on_incomparable - ## ALIAS first, last, slice, sample + ## ALIAS first, last, sample, slice GROUP Selections Creates a new `Vector` with only the specified range of elements from the input, removing any elements outside the range. @@ -515,7 +515,7 @@ type Array map_with_index : (Integer -> Any -> Any) -> Vector Any map_with_index self function = Vector.map_with_index self function - ## GROUP Selections + ## PRIVATE Creates a new array with the skipping elements until `start` and then continuing until `end` index. @@ -674,7 +674,7 @@ type Array contains : Any -> Boolean contains self elem = self.any (== elem) - ## ALIAS combine, merge, join by row position + ## ALIAS combine, join by row position, merge GROUP Calculations Performs a pair-wise operation passed in `function` on consecutive elements of `self` and `that`. @@ -752,7 +752,7 @@ type Array each_with_index : (Integer -> Any -> Any) -> Nothing each_with_index self f = Vector.each_with_index self f - ## ALIAS concatenate, union, append + ## ALIAS append, concatenate, union GROUP Operators Concatenates two arrays, resulting in a new `Vector`, containing all the elements of `self`, followed by all the elements of `that`. diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Enso_Cloud/Enso_File.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Enso_Cloud/Enso_File.enso new file mode 100644 index 0000000000..b7ec6e24b1 --- /dev/null +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Enso_Cloud/Enso_File.enso @@ -0,0 +1,213 @@ +import project.Any.Any +import project.Data.Enso_Cloud.Utils +import project.Data.Index_Sub_Range.Index_Sub_Range +import project.Data.Json.JS_Object +import project.Data.Text.Encoding.Encoding +import project.Data.Text.Matching_Mode.Matching_Mode +import project.Data.Text.Text +import project.Data.Text.Text_Sub_Range.Text_Sub_Range +import project.Data.Vector.Vector +import project.Error.Error +import project.Errors.File_Error.File_Error +import project.Errors.Illegal_Argument.Illegal_Argument +import project.Errors.Problem_Behavior.Problem_Behavior +import project.Errors.Unimplemented.Unimplemented +import project.Network.HTTP.HTTP +import project.Network.HTTP.HTTP_Method.HTTP_Method +import project.Nothing.Nothing +import project.System.File.File_Access.File_Access +import project.System.File_Format.File_For_Read +import project.System.Input_Stream.Input_Stream +import project.System.Output_Stream.Output_Stream +from project.Data.Boolean import Boolean, False, True +from project.Data.Text.Extensions import all +from project.System.File_Format import Auto_Detect, File_Format, Bytes, Plain_Text_Format + +type Enso_File + ## PRIVATE + Represents a file or folder within the Enso cloud. + Value name:Text id:Text organisation:Text asset_type:Enso_Asset_Type + + ## Represents the root folder of the current users. + root : Enso_File + root = Enso_File.Value "" "" "" Enso_Asset_Type.Directory + + ## PRIVATE + Target URI for the api + internal_uri : Text + internal_uri self = case self.id of + "" -> if self.asset_type == Enso_Asset_Type.Directory then Utils.directory_api else + Error.throw (Illegal_Argument.Error "Invalid ID for a "+self.asset_type.to_text+".") + _ -> case self.asset_type of + Enso_Asset_Type.Directory -> Utils.directory_api + "?parent_id=" + self.id + Enso_Asset_Type.File -> Utils.files_api + "/" + self.id + Enso_Asset_Type.Project -> Utils.projects_api + "/" + self.id + Enso_Asset_Type.Data_Link -> Utils.secrets_api + "/" + self.id + Enso_Asset_Type.Secret -> Error.throw (Illegal_Argument.Error "Secrets cannot be accessed directly.") + + ## Checks if the folder or file exists + exists : Boolean + exists self = + auth_header = Utils.authorization_header + response = HTTP.fetch self.internal_uri HTTP_Method.Get [auth_header] + response.code.is_success + + ## Checks if this is a folder + is_directory : Boolean + is_directory self = self.asset_type == Enso_Asset_Type.Directory + + ## PRIVATE + ADVANCED + Creates a new output stream for this file and runs the specified action + on it. + + The created stream is automatically closed when `action` returns (even + if it returns exceptionally). + + Arguments: + - open_options: A vector of `File_Access` objects determining how to open + the stream. These options set the access properties of the stream. + - action: A function that operates on the output stream and returns some + value. The value is returned from this method. + with_output_stream : Vector File_Access -> (Output_Stream -> Any ! File_Error) -> Any ! File_Error + with_output_stream self open_options action = + _ = [open_options, action] + Unimplemented.throw "Writing to Enso_Files is not currently implemented." + + ## PRIVATE + ADVANCED + Creates a new input stream for this file and runs the specified action + on it. + + Arguments: + - open_options: A vector of `File_Access` objects determining how to open + the stream. These options set the access properties of the stream. + - action: A function that operates on the input stream and returns some + value. The value is returned from this method. + + The created stream is automatically closed when `action` returns (even + if it returns exceptionally). + with_input_stream : Vector File_Access -> (Input_Stream -> Any ! File_Error) -> Any ! File_Error | Illegal_Argument + with_input_stream self open_options action = if self.asset_type != Enso_Asset_Type.File then Error.throw (Illegal_Argument.Error "Only files can be opened as a stream.") else + if (open_options != [File_Access.Read]) then Error.throw (Illegal_Argument.Error "Files can only be opened for reading.") else + auth_header = Utils.authorization_header + response = HTTP.fetch self.internal_uri HTTP_Method.Get [auth_header] + response.if_not_error <| + js_object = response.decode_as_json + path = js_object.get "path" + if path.is_nothing then Error.throw (Illegal_Argument.Error "Invalid JSON for an Enso_File.") else + url = path.replace "s3://production-enso-organizations-files/" "https://production-enso-organizations-files.s3.eu-west-1.amazonaws.com/" + response = HTTP.fetch url HTTP_Method.Get [] + response.if_not_error <| response.with_stream action + + ## ALIAS load, open + GROUP Input + Read a file using the specified file format + + Arguments: + - format: A `File_Format` object used to read file into memory. + If `Auto_Detect` is specified; the provided file determines the specific + type and configures it appropriately. If there is no matching type then + a `File_Error.Unsupported_Type` error is returned. + - on_problems: Specifies the behavior when a problem occurs during the + function. + By default, a warning is issued, but the operation proceeds. + If set to `Report_Error`, the operation fails with a dataflow error. + If set to `Ignore`, the operation proceeds without errors or warnings. + @format File_Format.default_widget + read : File_Format -> Problem_Behavior -> Any ! Illegal_Argument | File_Error + read self format=Auto_Detect (on_problems=Problem_Behavior.Report_Warning) = case self.asset_type of + Enso_Asset_Type.Project -> Error.throw (Illegal_Argument.Error "Projects cannot be read within Enso code. Open using the IDE.") + Enso_Asset_Type.Secret -> Error.throw (Illegal_Argument.Error "Secrets cannot be read directly.") + Enso_Asset_Type.Data_Link -> Unimplemented.throw "Reading from a Data Link is not implemented yet." + Enso_Asset_Type.Directory -> if format == Auto_Detect then self.list else Error.throw (Illegal_Argument.Error "Directories can only be read using the Auto_Detect format.") + Enso_Asset_Type.File -> case format of + Auto_Detect -> + real_format = Auto_Detect.get_reading_format self + if real_format == Nothing then Error.throw (File_Error.Unsupported_Type self) else + self.read real_format on_problems + _ -> self.with_input_stream [File_Access.Read] format.read_stream + + ## ALIAS load bytes, open bytes + Reads all bytes in this file into a byte vector. + read_bytes : Vector ! File_Error + read_bytes self = + self.read Bytes + + ## ALIAS load text, open text + Reads the whole file into a `Text`, with specified encoding. + + Arguments: + - encoding: The text encoding to decode the file with. Defaults to UTF-8. + - on_problems: Specifies the behavior when a problem occurs during the + function. + By default, a warning is issued, but the operation proceeds. + If set to `Report_Error`, the operation fails with a dataflow error. + If set to `Ignore`, the operation proceeds without errors or warnings. + @encoding Encoding.default_widget + read_text : Encoding -> Problem_Behavior -> Text ! File_Error + read_text self (encoding=Encoding.utf_8) (on_problems=Problem_Behavior.Report_Warning) = + self.read (Plain_Text_Format.Plain_Text encoding) on_problems + + ## GROUP Metadata + Returns the extension of the file. + extension : Text + extension self = case self.asset_type of + Enso_Asset_Type.Directory -> Error.throw (Illegal_Argument.Error "Directories do not have extensions.") + Enso_Asset_Type.Secret -> Error.throw (Illegal_Argument.Error "Secrets cannot be accessed directly.") + Enso_Asset_Type.Project -> ".enso" + _ -> + name = self.name + last_dot = name.locate "." mode=Matching_Mode.Last + if last_dot.is_nothing then "" else + extension = name.drop (Index_Sub_Range.First last_dot.start) + if extension == "." then "" else extension + + ## Gets a list of assets within self. + list : Vector Enso_File + list self = if self.asset_type != Enso_Asset_Type.Directory then Error.throw (Illegal_Argument.Error "Only directories can be listed.") else + auth_header = Utils.authorization_header + response = HTTP.fetch self.internal_uri HTTP_Method.Get [auth_header] + response.if_not_error <| + js_object = response.decode_as_json + assets = js_object.get "assets" [] + files = assets.map t-> t.into Enso_File + + ## Remove secrets from the list + files.filter f-> f.asset_type != Enso_Asset_Type.Secret + +## PRIVATE +Enso_File.from (that:JS_Object) = if ["title", "id", "parentId"].any (k-> that.contains_key k . not) then Error.throw (Illegal_Argument.Error "Invalid JSON for an Enso_File.") else + name = that.get "title" + id = that.get "id" + org = that.get "organizationId" "" + asset_type = (id.take (Text_Sub_Range.Before "-")):Enso_Asset_Type + Enso_File.Value name id org asset_type + +type Enso_Asset_Type + ## Represents an Enso project. + Project + + ## Represents an file. + File + + ## Represents a folder. + Directory + + ## Represents a secret. + Secret + + ## Represents a connection to another data source. + Data_Link + +## PRIVATE +Enso_Asset_Type.from (that:Text) = case that of + "project" -> Enso_Asset_Type.Project + "file" -> Enso_Asset_Type.File + "directory" -> Enso_Asset_Type.Directory + "secret" -> Enso_Asset_Type.Secret + "connection" -> Enso_Asset_Type.Data_Link + _ -> Error.throw (Illegal_Argument.Error "Invalid asset type.") + +## PRIVATE +File_For_Read.from (that:Enso_File) = File_For_Read.Value that.uri that.name that.extension diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Enso_Cloud/Enso_Secret.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Enso_Cloud/Enso_Secret.enso new file mode 100644 index 0000000000..032ae846bb --- /dev/null +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Enso_Cloud/Enso_Secret.enso @@ -0,0 +1,94 @@ +import project.Data.Enso_Cloud.Enso_File.Enso_File +import project.Data.Enso_Cloud.Utils +import project.Data.Json.JS_Object +import project.Data.Text.Text +import project.Data.Vector.Vector +import project.Error.Error +import project.Errors.Common.Forbidden_Operation +import project.Errors.Common.Not_Found +import project.Errors.Illegal_Argument.Illegal_Argument +import project.Network.HTTP.HTTP +import project.Network.HTTP.HTTP_Method.HTTP_Method +import project.Network.HTTP.Request_Body.Request_Body +import project.Nothing.Nothing +import project.Runtime.Context +from project.Data.Boolean import Boolean, False, True +from project.Data.Text.Extensions import all + +type Enso_Secret + ## PRIVATE + Value name:Text id:Text + + ## Create a new secret. + + Arguments: + - name: The name of the secret + - value: The value of the secret + - parent: The parent folder for the secret. If `Nothing` then it will be + created in the root folder. + create : Text -> Text -> Enso_File | Nothing -> Enso_Secret + create name:Text value:Text parent:(Enso_File|Nothing)=Nothing = if name == "" then Error.throw (Illegal_Argument.Error "Secret name cannot be empty") else + if Context.Output.is_enabled.not then Error.throw (Forbidden_Operation.Error "Creating a secret is forbidden as the Output context is disabled.") else + if name.starts_with "connection-" then Error.throw (Illegal_Argument.Error "Secret name cannot start with 'connection-'") else + if Enso_Secret.exists name parent then Error.throw (Illegal_Argument.Error "Secret with this name already exists") else + auth_header = Utils.authorization_header + body = JS_Object.from_pairs [["secretName", name], ["secretValue", value]] + headers = if parent.is_nothing then [auth_header] else [auth_header, ["parent_id", Enso_File.id]] + response = HTTP.post Utils.secrets_api body HTTP_Method.Post headers + response.if_not_error <| + Enso_Secret.get name parent + + ## Deletes a secret. + delete : Enso_Secret + delete self = + if Context.Output.is_enabled.not then Error.throw (Forbidden_Operation.Error "Deleting a secret is forbidden as the Output context is disabled.") else + auth_header = Utils.authorization_header + uri = Utils.secrets_api + "/" + self.id + response = HTTP.post uri Request_Body.Empty HTTP_Method.Delete [auth_header] + response.if_not_error self + + ## Gets a list of all the secrets in the folder. + + Arguments: + - folder: The folder to get the secrets from. If `Nothing` then will get + the secrets from the root folder. + list : Enso_File | Nothing -> Vector Enso_Secret + list parent:(Enso_File|Nothing)=Nothing = + auth_header = Utils.authorization_header + auth_header.if_not_error <| + headers = if parent.is_nothing then [auth_header] else [auth_header, ["parent_id", Enso_File.id]] + response = HTTP.fetch Utils.secrets_api HTTP_Method.Get headers + response.if_not_error <| + js_object = response.decode_as_json + secrets = js_object.get "secrets" [] + raw_secrets = secrets.map v-> v.into Enso_Secret + raw_secrets.filter s-> (s.name.starts_with "connection-" == False) + + ## Get a Secret if it exists. + + Arguments: + - name: The name of the secret + - parent: The parent folder for the secret. If `Nothing` then will check + in the root folder. + get : Text -> Enso_File | Nothing -> Enso_Secret ! Not_Found + get name:Text parent:(Enso_File|Nothing)=Nothing = + Enso_Secret.list parent . find s-> s.name == name + + ## Checks if a Secret exists. + + Arguments: + - name: The name of the secret + - parent: The parent folder for the secret. If `Nothing` then will check + in the root folder. + exists : Text -> Enso_File | Nothing -> Boolean + exists name:Text parent:(Enso_File|Nothing)=Nothing = + Enso_Secret.list parent . any s-> s.name == name + +## PRIVATE +type Enso_Secret_Error + ## PRIVATE + Access_Denied + + ## PRIVATE + to_display_text : Text + to_display_text self = "Cannot read secret value into Enso." diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Enso_Cloud/Enso_User.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Enso_Cloud/Enso_User.enso new file mode 100644 index 0000000000..3fbd6c0ee2 --- /dev/null +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Enso_Cloud/Enso_User.enso @@ -0,0 +1,51 @@ +import project.Data.Enso_Cloud.Enso_File.Enso_Asset_Type +import project.Data.Enso_Cloud.Enso_File.Enso_File +import project.Data.Enso_Cloud.Utils +import project.Data.Json.JS_Object +import project.Data.Text.Text +import project.Data.Vector.Vector +import project.Error.Error +import project.Errors.Illegal_Argument.Illegal_Argument +import project.Network.HTTP.HTTP +import project.Network.HTTP.HTTP_Method.HTTP_Method +from project.Data.Boolean import Boolean, False, True + +type Enso_User + ## PRIVATE + Represents a user within Enso Cloud. + + Fields: + - name: The user's name. + - email: The user's email address. + - id: The user's unique ID. + - home: The user's home directory. + - is_enabled: Whether the user is enabled. + User name:Text email:Text id:Text home:Enso_File is_enabled:Boolean=True + + ## Fetch the current user. + current : Enso_User + current = + auth_header = Utils.authorization_header + user_api = Utils.cloud_root_uri + "users/me" + response = HTTP.fetch user_api HTTP_Method.Get [auth_header] + response.if_not_error <| + js_object = response.decode_as_json + js_object.into Enso_User + + ## Lists all known users. + list : Vector Enso_User + list = + auth_header = Utils.authorization_header + user_api = Utils.cloud_root_uri + "users" + response = HTTP.fetch user_api HTTP_Method.Get [auth_header] + response.if_not_error <| + js_object = response.decode_as_json + users = js_object.get 'users' [] + users.map (user-> user.into Enso_User) + +## PRIVATE +Enso_User.from (that:JS_Object) = if ["name", "email", "id"].any (k-> that.contains_key k . not) then Error.throw (Illegal_Argument.Error "Invalid JSON for an Enso_User.") else + root_folder_id = that.get "rootDirectoryId" "" + root_folder = Enso_File.Value "" root_folder_id "" Enso_Asset_Type.Directory + is_enabled = that.get "isEnabled" True + Enso_User.User (that.get "name") (that.get "email") (that.get "id") root_folder is_enabled diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Enso_Cloud/Utils.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Enso_Cloud/Utils.enso new file mode 100644 index 0000000000..49704853da --- /dev/null +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Enso_Cloud/Utils.enso @@ -0,0 +1,44 @@ +import project.Data.Pair.Pair +import project.Data.Text.Text +import project.Error.Error +import project.Nothing.Nothing +import project.Runtime.Ref.Ref +import project.System.File.File + +polyglot java import org.enso.base.enso_cloud.AuthenticationProvider + +## PRIVATE +cloud_root_uri = "" + AuthenticationProvider.getAPIRootURI + +## PRIVATE + Construct the authoization header for the request +authorization_header : Pair Text Text +authorization_header = + result = AuthenticationProvider.getToken.if_nothing <| + cred_file = File.home / ".enso" / "credentials" + if cred_file.exists.not then Error.throw Not_Logged_In else + AuthenticationProvider.setToken (cred_file.read_text) + Pair.new "Authorization" "Bearer "+result + +## PRIVATE + Root address for listing folders +directory_api = cloud_root_uri + "directories" + +## PRIVATE + Root address for listing folders +files_api = cloud_root_uri + "files" + +## PRIVATE + Root address for listing folders +projects_api = cloud_root_uri + "projects" + +## PRIVATE + Root address for Secrets API +secrets_api = cloud_root_uri + "secrets" + +## PRIVATE + Error if the user is not logged into Enso Cloud. +type Not_Logged_In + ## PRIVATE + to_display_text : Text + to_display_text self = "Not logged into Enso cloud. Please log in and restart." diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Json.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Json.enso index 197ee522db..df2e0b97d0 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Json.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Json.enso @@ -215,7 +215,7 @@ type JS_Object into self target_type = case target_type of JS_Object -> self Vector -> self.to_vector - Map -> Map.from_pairs self.to_vector + Map -> Map.from_vector self.to_vector _ -> ## First try a conversion Panic.catch No_Such_Conversion (self.to target_type) _-> diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Numbers.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Numbers.enso index 52daf14700..03df8c6fa5 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Numbers.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Numbers.enso @@ -1171,6 +1171,7 @@ type Number_Parse_Error to_display_text self = "Could not parse " + self.text.to_text + " as a double." +## PRIVATE Float.from (that:Integer) = (1.0 * that):Float ## A wrapper type that ensures that a function may only take positive integers. @@ -1190,7 +1191,8 @@ type Positive_Integer if integer > 0 then Positive_Integer.Value integer else Error.throw (Illegal_Argument.Error "Expected a positive integer, but got "+integer.to_display_text) -## Allows to create a `Positive_Integer` from an `Integer`. +## PRIVATE + Allows to create a `Positive_Integer` from an `Integer`. It will throw `Illegal_Argument` if the provided integer is not positive. Positive_Integer.from (that : Integer) = Positive_Integer.new that diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Text/Extensions.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Text/Extensions.enso index 71583ef564..6af8017c0f 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Text/Extensions.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Text/Extensions.enso @@ -30,7 +30,6 @@ import project.Errors.Illegal_Argument.Illegal_Argument import project.Errors.Problem_Behavior.Problem_Behavior import project.Errors.Time_Error.Time_Error import project.Meta -import project.Network.URI.URI import project.Nothing.Nothing from project.Data.Boolean import Boolean, False, True from project.Data.Json import Invalid_JSON, JS_Object, Json diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Date_Time_Formatter.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Date_Time_Formatter.enso index 7f00340876..f0268a2955 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Date_Time_Formatter.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Date_Time_Formatter.enso @@ -261,7 +261,8 @@ type Date_Time_Formatter iso_time = Date_Time_Formatter.Value (EnsoDateTimeFormatter.makeISOConstant DateTimeFormatter.ISO_TIME "iso_time") - ## Returns a text representation of this formatter. + ## PRIVATE + Returns a text representation of this formatter. to_text : Text to_text self = case self.underlying.getFormatterKind of FormatterKind.CONSTANT -> @@ -275,7 +276,8 @@ type Date_Time_Formatter original_pattern : Text -> "Date_Time_Formatter.from_java " + original_pattern.pretty Nothing -> "Date_Time_Formatter.from_java " + self.underlying.getFormatter.to_text - ## Parses a human-readable representation of this formatter. + ## PRIVATE + Parses a human-readable representation of this formatter. to_display_text : Text to_display_text self = self.to_text diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Day_Of_Week.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Day_Of_Week.enso index 634dbda42a..83f530f64c 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Day_Of_Week.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Time/Day_Of_Week.enso @@ -79,7 +79,8 @@ type Day_Of_Week_Comparator ## PRIVATE Comparable.from (_:Day_Of_Week) = Day_Of_Week_Comparator -## Convert from an integer to a Day_Of_Week +## PRIVATE + Convert from an integer to a Day_Of_Week Arguments: - `that`: The first day of the week. diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Vector.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Vector.enso index 8f349aba58..096355357f 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Vector.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/Vector.enso @@ -710,7 +710,7 @@ type Vector a "and " + remaining_count.to_text + " more elements" prefix.map .to_text . join ", " "[" " "+remaining_text+"]" - ## ALIAS concatenate, union, append + ## ALIAS append, concatenate, union GROUP Operators Concatenates two vectors, resulting in a new `Vector`, containing all the elements of `self`, followed by all the elements of `that`. @@ -723,10 +723,9 @@ type Vector a [1] + [2] + : Vector Any -> Vector Any - + self that = case that of + + self that:(Array|Vector) = case that of _ : Vector -> Vector.insert_builtin self self.length that _ : Array -> self + Vector.from_polyglot_array that - _ -> Error.throw (Type_Error.Error Vector that "that") ## GROUP Calculations Inserts the given item into the vector at the given index. @@ -807,7 +806,7 @@ type Vector a slice : Integer -> Integer -> Vector Any slice self start end = @Builtin_Method "Vector.slice" - ## ALIAS first, last, slice, sample + ## ALIAS first, last, sample, slice GROUP Selections Creates a new `Vector` with only the specified range of elements from the input, removing any elements outside the range. @@ -843,7 +842,7 @@ type Vector a drop self range=(Index_Sub_Range.First 1) = drop_helper self.length (self.at _) self.slice (slice_ranges self) range - ## ALIAS combine, merge, join by row position + ## ALIAS combine, join by row position, merge GROUP Calculations Performs a pair-wise operation passed in `function` on consecutive elements of `self` and `that`. diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/XML.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/XML.enso index 7b9b93c9e9..d8a3804069 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/XML.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/XML.enso @@ -21,10 +21,11 @@ from project.Data.Text.Extensions import all polyglot java import java.io.StringReader polyglot java import java.lang.Exception as JException -polyglot java import javax.xml.parsers.DocumentBuilderFactory polyglot java import javax.xml.parsers.DocumentBuilder +polyglot java import javax.xml.parsers.DocumentBuilderFactory polyglot java import javax.xml.xpath.XPathConstants polyglot java import javax.xml.xpath.XPathFactory +polyglot java import org.enso.base.XML_Utils polyglot java import org.w3c.dom.Document polyglot java import org.w3c.dom.Element polyglot java import org.w3c.dom.Node @@ -34,8 +35,6 @@ polyglot java import org.xml.sax.InputSource polyglot java import org.xml.sax.SAXException polyglot java import org.xml.sax.SAXParseException -polyglot java import org.enso.base.XML_Utils - type XML_Document ## Read an XML document from a file. diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/XML/XML_Format.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/XML/XML_Format.enso index cf4b607b6a..4a33154dc2 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Data/XML/XML_Format.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Data/XML/XML_Format.enso @@ -5,6 +5,7 @@ import project.Errors.Problem_Behavior.Problem_Behavior import project.Network.URI.URI import project.Nothing.Nothing import project.System.File.File +import project.System.File_Format.File_For_Read import project.System.Input_Stream.Input_Stream from project.Data.Text.Extensions import all @@ -12,8 +13,8 @@ from project.Data.Text.Extensions import all type XML_Format ## PRIVATE If the File_Format supports reading from the file, return a configured instance. - for_file_read : File -> XML_Format | Nothing - for_file_read file:File = + for_file_read : File_For_Read -> XML_Format | Nothing + for_file_read file:File_For_Read = case file.extension of ".xml" -> XML_Format _ -> Nothing diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Errors/File_Error.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Errors/File_Error.enso index 0ba302f9a1..5425b6c0f6 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Errors/File_Error.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Errors/File_Error.enso @@ -5,6 +5,7 @@ import project.Meta import project.Nothing.Nothing import project.Panic.Panic import project.System.File.File +import project.System.File_Format.File_For_Read import project.System.File_Format.File_Format polyglot java import java.io.IOException @@ -35,7 +36,7 @@ type File_Error IO_Error (file : File) (message : Text) ## Indicates that the given file's type is not supported. - Unsupported_Type (file : File) + Unsupported_Type (file : File_For_Read) ## Indicates that the given type cannot be serialized to the provided file format. diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Main.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Main.enso index af9ebc1058..b24a890c2d 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Main.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Main.enso @@ -2,6 +2,9 @@ import project.Any.Any import project.Data import project.Data.Array.Array import project.Data.Boolean +import project.Data.Enso_Cloud.Enso_File.Enso_File +import project.Data.Enso_Cloud.Enso_Secret.Enso_Secret +import project.Data.Enso_Cloud.Enso_User.Enso_User import project.Data.Filter_Condition.Filter_Condition import project.Data.Index_Sub_Range.Index_Sub_Range import project.Data.Interval.Bound @@ -96,6 +99,9 @@ from project.System.File_Format.Plain_Text_Format import Plain_Text export project.Any.Any export project.Data export project.Data.Array.Array +export project.Data.Enso_Cloud.Enso_File.Enso_File +export project.Data.Enso_Cloud.Enso_Secret.Enso_Secret +export project.Data.Enso_Cloud.Enso_User.Enso_User export project.Data.Filter_Condition.Filter_Condition export project.Data.Index_Sub_Range.Index_Sub_Range export project.Data.Interval.Bound diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Meta.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Meta.enso index 4bdec3a6cd..a97e99134e 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Meta.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Meta.enso @@ -11,11 +11,11 @@ import project.Data.Time.Time_Zone.Time_Zone import project.Data.Vector.Vector import project.Error.Error as Base_Error import project.Errors.Common.Not_Found -import project.Nothing.Nothing import project.Function.Function +import project.Nothing.Nothing import project.Polyglot.Java -from project.Runtime.Managed_Resource import Managed_Resource from project.Data.Boolean import Boolean, False, True +from project.Runtime.Managed_Resource import Managed_Resource type Type ## PRIVATE @@ -588,6 +588,7 @@ get_short_type_name typ = @Builtin_Method "Meta.get_short_type_name" get_constructor_declaring_type : Any -> Any get_constructor_declaring_type constructor = @Builtin_Method "Meta.get_constructor_declaring_type" +## PRIVATE instrumentor_builtin op args = @Builtin_Method "Meta.instrumentor_builtin" ## PRIVATE diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Network/Extensions.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Network/Extensions.enso index a876c2857e..30434b9053 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Network/Extensions.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Network/Extensions.enso @@ -8,6 +8,7 @@ import project.Network.HTTP.Header.Header import project.Network.HTTP.HTTP_Method.HTTP_Method import project.Network.HTTP.Request_Body.Request_Body import project.Network.URI.URI +import project.Network.URI_With_Query.URI_With_Query from project.Data.Boolean import Boolean, False, True ## ALIAS parse_uri, uri from text @@ -42,6 +43,23 @@ URI.fetch : HTTP_Method -> Vector (Header | Pair Text Text) -> Boolean -> Any URI.fetch self (method:HTTP_Method=HTTP_Method.Get) headers=[] try_auto_parse_response=True = Data.fetch self method headers try_auto_parse_response +## ALIAS download, http get + GROUP Input + Fetches from the URI 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. + - try_auto_parse_response: If successful should the body be attempted to be + parsed to an Enso native object. +URI_With_Query.fetch : HTTP_Method -> Vector (Header | Pair Text Text) -> Boolean -> Any +URI_With_Query.fetch self (method:HTTP_Method=HTTP_Method.Get) headers=[] try_auto_parse_response=True = + Data.fetch self method headers try_auto_parse_response + ## ALIAS upload, http post GROUP Input Writes the provided data to the provided URI. Returns the response, @@ -81,3 +99,43 @@ URI.fetch self (method:HTTP_Method=HTTP_Method.Get) headers=[] try_auto_parse_re URI.post : Request_Body -> HTTP_Method -> Vector (Header | Pair Text Text) -> Boolean -> Any URI.post self (body:Request_Body=Request_Body.Empty) (method:HTTP_Method=HTTP_Method.Post) (headers:(Vector (Header | Pair Text Text))=[]) (try_auto_parse_response:Boolean=True) = Data.post self body method headers try_auto_parse_response + +## ALIAS upload, http post + GROUP Input + Writes the provided data to the provided URI. 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: + - 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. + - try_auto_parse_response: If successful should the body be attempted to be + parsed to an Enso native object. + + ! Specifying Content Types + + If the `body` parameter specifies an explicit content type, then it is an + error to also specify additional `Content-Type` headers in the `headers` + parameter. (It is not an error to specify multiple `Content-Type` values in + `headers`, however.) + + ! Supported Body Types + + - Request_Body.Text: Sends a text string, with optional encoding and content + type. + - Request_Body.Json: Sends an Enso object, after converting it to JSON. + - Request_Body.Binary: Sends a file. + - Request_Body.Form_Data: Sends a form encoded as key/value pairs. The keys + must be `Text`, and the values must be `Text` or `File`. + - Request_Body.Empty: Sends an empty body. + + Additionally, the following types are allowed as the `body` parameter: + + - Text: shorthand for `Request_Body.Text that_text`. + - File: shorthand for `Request_Body.Binary that_file`. + - Any other Enso object: shorthand for `Request_Body.Json that_object`. +URI_With_Query.post : Request_Body -> HTTP_Method -> Vector (Header | Pair Text Text) -> Boolean -> Any +URI_With_Query.post self (body:Request_Body=Request_Body.Empty) (method:HTTP_Method=HTTP_Method.Post) (headers:(Vector (Header | Pair Text Text))=[]) (try_auto_parse_response:Boolean=True) = + Data.post self body method headers try_auto_parse_response diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Network/HTTP.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Network/HTTP.enso index 5b279e9144..cc9b535f5f 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Network/HTTP.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Network/HTTP.enso @@ -1,4 +1,5 @@ import project.Any.Any +import project.Data.Enso_Cloud.Enso_Secret.Enso_Secret import project.Data.Map.Map import project.Data.Pair.Pair import project.Data.Set.Set @@ -18,6 +19,7 @@ import project.Network.HTTP.Request_Body.Request_Body import project.Network.HTTP.Response.Response import project.Network.Proxy.Proxy import project.Network.URI.URI +import project.Network.URI_With_Query.URI_With_Query import project.Nothing.Nothing import project.Panic.Panic import project.Runtime.Context @@ -29,11 +31,13 @@ polyglot java import java.lang.Exception as JException polyglot java import java.net.http.HttpClient polyglot java import java.net.http.HttpRequest polyglot java import java.net.http.HttpRequest.BodyPublisher -polyglot java import java.net.http.HttpResponse polyglot java import java.net.InetSocketAddress polyglot java import java.net.ProxySelector polyglot java import java.nio.file.Path -polyglot java import org.enso.base.Http_Utils +polyglot java import org.enso.base.enso_cloud.EnsoKeyValuePair +polyglot java import org.enso.base.enso_cloud.EnsoSecretHelper +polyglot java import org.enso.base.net.http.MultipartBodyBuilder +polyglot java import org.enso.base.net.http.UrlencodedBodyBuilder type HTTP ## PRIVATE @@ -64,7 +68,7 @@ type HTTP example_new = HTTP.new (timeout = (Duration.new seconds=30)) (proxy = Proxy.Address "example.com" 8080) new : Duration -> Boolean -> Proxy -> HTTP_Version -> HTTP - new (timeout = (Duration.new seconds=10)) (follow_redirects = True) (proxy = Proxy.System) (version = HTTP_Version.HTTP_1_1) = + new (timeout:Duration=(Duration.new seconds=10)) (follow_redirects:Boolean=True) (proxy:Proxy=Proxy.System) (version:HTTP_Version=HTTP_Version.HTTP_2) = HTTP.Value timeout follow_redirects proxy version ## PRIVATE @@ -97,72 +101,49 @@ type HTTP Panic.catch JException handler=(cause-> Error.throw (Request_Error.Error 'IllegalArgumentException' cause.payload.getMessage)) Panic.recover Any <| handle_request_error <| check_output_context <| - body_publishers = HttpRequest.BodyPublishers - builder = HttpRequest.newBuilder - - # set uri - uri = case req.uri of - _ : Text -> req.uri.to_uri - _ : URI -> req.uri - builder.uri uri.internal_uri - headers = resolve_headers req - headers.if_not_error <| - # Generate body publisher and optional form content boundary - body_publisher_and_boundary = case req.body of - Request_Body.Text text encoding _ -> - body_publisher = case encoding of - Nothing -> body_publishers.ofString text - _ : Encoding -> body_publishers.ofString text encoding.to_java_charset - Pair.new body_publisher Nothing - Request_Body.Json x -> - json = x.to_json - json.if_not_error <| - Pair.new (body_publishers.ofString json) Nothing - Request_Body.Binary file -> - path = Path.of file.path - Pair.new (body_publishers.ofFile path) Nothing - Request_Body.Form_Data form_data url_encoded -> - build_form_body_publisher form_data url_encoded - Request_Body.Empty -> - Pair.new (body_publishers.noBody) Nothing - _ -> - Error.throw (Illegal_Argument.Error ("Unsupported POST body: " + req.body.to_display_text + "; this is a bug in the Data library")) - - # Send request + body_publisher_and_boundary = resolve_body_to_publisher_and_boundary req.body body_publisher_and_boundary.if_not_error <| - body_publisher = body_publisher_and_boundary.first + # Create builder and set method and body + builder = HttpRequest.newBuilder + builder.method req.method.to_http_method_name body_publisher_and_boundary.first + + # Create Unified Header list boundary = body_publisher_and_boundary.second - boundary_header_list = if boundary.is_nothing then [] else [Header.multipart_form_data boundary] - - # set method and body - builder.method req.method.to_http_method_name body_publisher - - # set headers all_headers = headers + boundary_header_list - all_headers.map h-> builder.header h.name h.value + mapped_headers = all_headers.map h-> case h.value of + _ : Enso_Secret -> EnsoKeyValuePair.ofSecret h.name h.value.id + _ -> EnsoKeyValuePair.ofText h.name h.value - http_request = builder.build - body_handler = HttpResponse.BodyHandlers . ofInputStream + # Get uri and resolve query arguments + uri_args = case req.uri of + _ : Text -> [req.uri.to_uri, []] + _ : URI -> [req.uri, []] + _ : URI_With_Query -> + mapped_arguments = req.uri.parameters.map arg-> case arg.second of + _ : Enso_Secret -> EnsoKeyValuePair.ofSecret arg.first arg.second.id + _ : Text -> EnsoKeyValuePair.ofText arg.first arg.second + _ -> Error.throw (Illegal_Argument.Error "Invalid query argument type - all values must be Vector or Pair.") + [req.uri.uri, mapped_arguments] - response = Response.Value (self.internal_http_client.send http_request body_handler) + response = Response.Value (EnsoSecretHelper.makeRequest self.internal_http_client builder uri_args.first.internal_uri uri_args.second mapped_headers) if error_on_failure_code.not || response.code.is_success then response else Error.throw (Request_Error.Error "Status Code" ("Request failed with status code: " + response.code.to_text + ". " + response.body.decode_as_text)) ## PRIVATE Static helper for get-like methods - fetch : (URI | Text) -> HTTP_Method -> Vector (Header | Pair Text Text) -> Any - fetch (uri:(URI | Text)) (method:HTTP_Method=HTTP_Method.Get) (headers:(Vector (Header | Pair Text Text))=[]) = + fetch : (URI | URI_With_Query | Text) -> HTTP_Method -> Vector (Header | Pair Text Text) -> Any + fetch (uri:(URI | URI_With_Query | Text)) (method:HTTP_Method=HTTP_Method.Get) (headers:(Vector (Header | Pair Text Text))=[]) = check_method fetch_methods method <| request = Request.new method uri (parse_headers headers) Request_Body.Empty HTTP.new.request request ## PRIVATE Static helper for post-like methods - post : (URI | Text) -> Request_Body -> HTTP_Method -> Vector (Header | Pair Text Text) -> Any - post (uri:(URI | Text)) (body:Request_Body=Request_Body.Empty) (method:HTTP_Method=HTTP_Method.Post) (headers:(Vector (Header | Pair Text Text))=[]) = + post : (URI | URI_With_Query | Text) -> Request_Body -> HTTP_Method -> Vector (Header | Pair Text Text) -> Any + post (uri:(URI | URI_With_Query | Text)) (body:Request_Body=Request_Body.Empty) (method:HTTP_Method=HTTP_Method.Post) (headers:(Vector (Header | Pair Text Text))=[]) = check_method post_methods method <| request = Request.new method uri (parse_headers headers) body HTTP.new.request request @@ -172,31 +153,20 @@ type HTTP Build an HTTP client. internal_http_client : HttpClient internal_http_client self = - builder = HttpClient.newBuilder - builder.connectTimeout self.timeout - # redirect - redirect = HttpClient.Redirect - redirect_policy = case self.follow_redirects of - True -> redirect.ALWAYS - False -> redirect.NEVER + builder = HttpClient.newBuilder.connectTimeout self.timeout + + redirect_policy = if self.follow_redirects then HttpClient.Redirect.ALWAYS else HttpClient.Redirect.NEVER builder.followRedirects redirect_policy - # proxy + case self.proxy of - Proxy.Address proxy_host proxy_port -> - proxy_selector = ProxySelector.of (InetSocketAddress.new proxy_host proxy_port) - builder.proxy proxy_selector - Proxy.System -> - proxy_selector = ProxySelector.getDefault - builder.proxy proxy_selector - Proxy.None -> - Nothing - # version + Proxy.Address proxy_host proxy_port -> builder.proxy (ProxySelector.of (InetSocketAddress.new proxy_host proxy_port)) + Proxy.System -> builder.proxy ProxySelector.getDefault + Proxy.None -> Nothing + case self.version of - HTTP_Version.HTTP_1_1 -> - builder.version HttpClient.Version.HTTP_1_1 - HTTP_Version.HTTP_2 -> - builder.version HttpClient.Version.HTTP_2 - # build http client + HTTP_Version.HTTP_1_1 -> builder.version HttpClient.Version.HTTP_1_1 + HTTP_Version.HTTP_2 -> builder.version HttpClient.Version.HTTP_2 + builder.build ## PRIVATE @@ -249,6 +219,32 @@ resolve_headers req = all_headers + default_content_type +## PRIVATE + Generate body publisher and optional form content boundary +resolve_body_to_publisher_and_boundary : Request_Body -> Pair BodyPublisher Text +resolve_body_to_publisher_and_boundary body:Request_Body = + body_publishers = HttpRequest.BodyPublishers + case body of + Request_Body.Text text encoding _ -> + body_publisher = case encoding of + Nothing -> body_publishers.ofString text + _ : Encoding -> body_publishers.ofString text encoding.to_java_charset + Pair.new body_publisher Nothing + Request_Body.Json x -> + json = x.to_json + json.if_not_error <| + Pair.new (body_publishers.ofString json) Nothing + Request_Body.Binary file -> + path = Path.of file.path + Pair.new (body_publishers.ofFile path) Nothing + Request_Body.Form_Data form_data url_encoded -> + build_form_body_publisher form_data url_encoded + Request_Body.Empty -> + Pair.new (body_publishers.noBody) Nothing + _ -> + Error.throw (Illegal_Argument.Error ("Unsupported POST body: " + body.to_display_text + "; this is a bug in the Data library")) + + ## PRIVATE Build a BodyPublisher from the given form data. @@ -256,14 +252,14 @@ resolve_headers req = build_form_body_publisher : Map Text (Text | File) -> Boolean -> Pair BodyPublisher Text build_form_body_publisher (form_data:(Map Text (Text | File))) (url_encoded:Boolean=False) = case url_encoded of True -> - body_builder = Http_Utils.urlencoded_body_builder + body_builder = UrlencodedBodyBuilder.new form_data.map_with_key key-> value-> case value of _ : Text -> body_builder.add_part_text key value _ : File -> body_builder.add_part_file key value.path Pair.new body_builder.build Nothing False -> - body_builder = Http_Utils.multipart_body_builder + body_builder = MultipartBodyBuilder.new form_data.map_with_key key-> value-> case value of _ : Text -> body_builder.add_part_text key value diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Network/HTTP/HTTP_Error.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Network/HTTP/HTTP_Error.enso index 929bb528a8..09d75fa1b8 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Network/HTTP/HTTP_Error.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Network/HTTP/HTTP_Error.enso @@ -1,6 +1,7 @@ import project.Any.Any import project.Data.Text.Text import project.Network.URI.URI +import project.Network.URI_With_Query.URI_With_Query import project.Panic.Panic polyglot java import java.io.IOException @@ -12,7 +13,7 @@ type HTTP_Error Arguments: - uri: The uri that couldn't be read. - message: The message for the error. - IO_Error (uri:URI) (message:Text) + IO_Error (uri:URI|URI_With_Query) (message:Text) ## PRIVATE Convert the HTTP_Error to a human-readable format. @@ -21,6 +22,6 @@ type HTTP_Error ## PRIVATE Utility method for running an action with Java exceptions mapping. - handle_java_exceptions uri:URI ~action = + handle_java_exceptions (uri:URI|URI_With_Query) ~action = Panic.catch IOException action caught_panic-> HTTP_Error.IO_Error uri ("An IO error has occurred: " + caught_panic.payload.to_text) diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Network/HTTP/Header.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Network/HTTP/Header.enso index 9723df8259..3dbfa640d1 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Network/HTTP/Header.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Network/HTTP/Header.enso @@ -1,11 +1,13 @@ +import project.Data.Enso_Cloud.Enso_Secret.Enso_Secret import project.Data.Numbers.Integer import project.Data.Text.Encoding.Encoding import project.Data.Text.Text import project.Nothing.Nothing from project.Data.Boolean import Boolean, False, True from project.Data.Ordering import all +from project.Data.Text.Extensions import all -polyglot java import org.enso.base.Http_Utils +polyglot java import java.util.Base64 type Header ## PRIVATE @@ -15,7 +17,7 @@ type Header Arguments: - name: The header name. - value: The header value. - Value name value + Value name:Text value:(Text|Enso_Secret) ## ALIAS Build a Header @@ -31,8 +33,8 @@ type Header import Standard.Base.Network.HTTP.Header.Header example_new = Header.new "My_Header" "my header's value" - new : Text -> Text -> Header - new name value = Header.Value name value + new : Text -> Text | Enso_Secret -> Header + new name:Text value:(Text|Enso_Secret) = Header.Value name value ## Create an "Accept" header. @@ -89,14 +91,13 @@ type Header example_auth_basic = Header.authorization_basic "user" "pass" authorization_basic : Text -> Text -> Header authorization_basic user pass = - Header.authorization (Http_Utils.header_basic_auth user pass) + Header.authorization (make_basic_auth user pass) ## Create bearer token auth header. Arguments: - token: The token. - authorization_bearer : Text -> Header - authorization_bearer token = + authorization_bearer token:Text = Header.authorization ("Bearer " + token) ## Create "Content-Type" header. @@ -113,8 +114,8 @@ type Header import Standard.Base.Network.HTTP.Header.Header example_content_type = Header.content_type "my_type" - content_type : Text -> Encoding -> Header - content_type value encoding=Nothing = + content_type : Text -> Encoding | Nothing -> Header + content_type value:Text encoding:(Encoding|Nothing)=Nothing = charset = if encoding.is_nothing then "" else "; charset="+encoding.character_set Header.Value Header.content_type_header_name value+charset @@ -163,7 +164,7 @@ type Header example_multipart = Header.multipart_form_data multipart_form_data : Text -> Header - multipart_form_data (boundary = "") = + multipart_form_data (boundary:Text="") = if boundary == "" then Header.content_type "multipart/form-data" else Header.content_type ("multipart/form-data; boundary=" + boundary) @@ -184,7 +185,8 @@ type Header to_display_text : Text to_display_text self = self.name + ": " + self.value.to_display_text - ## Gets the name for content_type + ## PRIVATE + Gets the name for content_type content_type_header_name : Text content_type_header_name = "Content-Type" @@ -204,3 +206,9 @@ type Header_Comparator ## PRIVATE Comparable.from (_:Header) = Header_Comparator + +## PRIVATE +make_basic_auth : Text -> Text -> Text +make_basic_auth user:Text pass:Text = + base64 = Base64.getEncoder.encodeToString (user + ":" + pass).utf_8 + "Basic " + base64 diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Network/HTTP/Request.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Network/HTTP/Request.enso index c737fdbe24..7c7848be8f 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Network/HTTP/Request.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Network/HTTP/Request.enso @@ -10,6 +10,7 @@ import project.Network.HTTP.Header.Header import project.Network.HTTP.HTTP_Method.HTTP_Method import project.Network.HTTP.Request_Body.Request_Body import project.Network.URI.URI +import project.Network.URI_With_Query.URI_With_Query import project.Nothing.Nothing import project.Panic.Panic from project.Data.Boolean import Boolean, False, True @@ -32,9 +33,9 @@ type Request import Standard.Base.Network.URI.URI example_new = Request.new Method.Post (URI.parse "http://example.com") - new : HTTP_Method -> (Text | URI) -> Vector Header -> Request_Body -> Request - new (method:HTTP_Method) (url:(Text | URI)) (headers:(Vector Header)=[]) (body:Request_Body=Request_Body.Empty) = - Panic.recover Any (Request.Value method (Panic.rethrow (url.to_uri)) headers body) + new : HTTP_Method -> (URI | URI_With_Query) -> Vector Header -> Request_Body -> Request + new (method:HTTP_Method) (url:(URI | URI_With_Query)) (headers:(Vector Header)=[]) (body:Request_Body=Request_Body.Empty) = + Request.Value method url headers body ## Create an Options request. @@ -47,7 +48,7 @@ type Request import Standard.Base.Network.URI.URI example_options = Request.options (URI.parse "http://example.com") - options : (Text | URI) -> Vector -> Request + options : (Text | URI | URI_With_Query) -> Vector -> Request options url (headers = []) = Request.new HTTP_Method.Options url headers ## Create a Get request. @@ -63,7 +64,7 @@ type Request import Standard.Base.Network.URI.URI example_get = Request.get (URI.parse "http://example.com") - get : (Text | URI) -> Vector -> Request + get : (Text | URI | URI_With_Query) -> Vector -> Request get url (headers = []) = Request.new HTTP_Method.Get url headers ## Create a Head request. @@ -79,7 +80,7 @@ type Request import Standard.Base.Network.URI.URI example_head = Request.head (URI.parse "http://example.com") - head : (Text | URI) -> Vector -> Request + head : (Text | URI | URI_With_Query) -> Vector -> Request head url (headers = []) = Request.new HTTP_Method.Head url headers ## Create a Post request. @@ -97,7 +98,7 @@ type Request import Standard.Base.Network.URI.URI example_post = Request.post (URI.parse "http://example.com") Request_Body.Empty - post : (Text | URI) -> Request_Body -> Vector -> Request + post : (Text | URI | URI_With_Query) -> Request_Body -> Vector -> Request post url body (headers = []) = Request.new HTTP_Method.Post url headers body ## Create a Put request. @@ -115,7 +116,7 @@ type Request import Standard.Base.Network.URI.URI example_put = Request.put (URI.parse "http://example.com") Request_Body.Empty - put : (Text | URI) -> Request_Body -> Vector -> Request + put : (Text | URI | URI_With_Query) -> Request_Body -> Vector -> Request put url body (headers = []) = Request.new HTTP_Method.Put url headers body ## Create a Delete request. @@ -131,7 +132,7 @@ type Request import Standard.Base.Network.URI.URI example_delete = Request.delete (URI.parse "http://example.com") - delete : (Text | URI) -> Vector -> Request + delete : (Text | URI | URI_With_Query) -> Vector -> Request delete url (headers = []) = Request.new HTTP_Method.Delete url headers ## PRIVATE diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Network/HTTP/Response.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Network/HTTP/Response.enso index ba23b3a408..93276773ae 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Network/HTTP/Response.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Network/HTTP/Response.enso @@ -18,8 +18,6 @@ import project.System.File_Format.File_Format from project.Data.Text.Extensions import all from project.Network.HTTP.Response_Body import decode_format_selector -polyglot java import org.enso.base.Http_Utils - type Response ## PRIVATE @@ -57,8 +55,8 @@ type Response example_headers = Examples.get_response.headers headers : Vector headers self = - header_entries = Vector.from_polyglot_array (Http_Utils.get_headers self.internal_http_response.headers) - header_entries.map e-> Header.new e.getKey e.getValue + header_keys = self.internal_http_response.headerNames + header_keys.flat_map k-> (self.internal_http_response.headers.allValues k).map v-> Header.new k v ## Get the response content type. content_type : Text | Nothing diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Network/HTTP/Response_Body.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Network/HTTP/Response_Body.enso index 185fa5f66e..ad99d448a8 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Network/HTTP/Response_Body.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Network/HTTP/Response_Body.enso @@ -40,7 +40,8 @@ maximum_body_in_memory = 4192 ## PRIVATE type Response_Body - ## Create a Response_Body. + ## PRIVATE + Create a Response_Body. Arguments: - stream: The body of the response as an InputStream. diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Network/URI.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Network/URI.enso index bb4aa80391..7f0b66ea2a 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Network/URI.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Network/URI.enso @@ -1,26 +1,16 @@ -import project.Any.Any -import project.Data.Boolean.Boolean +import project.Data.Enso_Cloud.Enso_Secret.Enso_Secret import project.Data.Json.JS_Object +import project.Data.Pair.Pair import project.Data.Text.Text +import project.Data.Vector.Vector import project.Error.Error import project.Errors.Common.Syntax_Error +import project.Network.URI_With_Query.URI_With_Query import project.Nothing.Nothing import project.Panic.Panic -polyglot java import java.lang.Exception as JException +polyglot java import java.lang.Exception polyglot java import java.net.URI as Java_URI -polyglot java import java.util.Optional - -## PRIVATE - - Handle a nothing value. - - Arguments: - - value: The value that may possibly be nothing. -handle_nothing : Any -> Any ! Nothing -handle_nothing value = case value of - Nothing -> Error.throw Nothing - _ -> value type URI ## ALIAS get uri @@ -41,7 +31,7 @@ type URI example_parse = URI.parse "http://example.com" parse : Text -> URI ! Syntax_Error parse uri:Text = - Panic.catch JException (URI.Value (Java_URI.create uri)) caught_panic-> + Panic.catch Exception (URI.Value (Java_URI.create uri)) caught_panic-> message = caught_panic.payload.getMessage truncated = if message.is_nothing || message.length > 100 then "Invalid URI '" + uri.to_display_text + "'" else "URI syntax error: " + message @@ -76,8 +66,8 @@ type URI import Standard.Examples example_scheme = Examples.uri.scheme - scheme : Text ! Nothing - scheme self = handle_nothing self.internal_uri.getScheme + scheme : Text | Nothing + scheme self = self.internal_uri.getScheme ## GROUP Metadata Get the user info part of this URI. @@ -88,8 +78,8 @@ type URI import Standard.Examples example_user_info = Examples.uri.user_info - user_info : Text ! Nothing - user_info self = handle_nothing self.internal_uri.getUserInfo + user_info : Text | Nothing + user_info self = self.internal_uri.getUserInfo ## GROUP Metadata Get the host part of this URI. @@ -100,8 +90,8 @@ type URI import Standard.Examples example_host = Examples.uri.host - host : Text ! Nothing - host self = handle_nothing self.internal_uri.getHost + host : Text | Nothing + host self = self.internal_uri.getHost ## Get the authority (user info and host) part of this URI. @@ -111,8 +101,8 @@ type URI import Standard.Examples example_authority = Examples.uri.authority - authority : Text ! Nothing - authority self = handle_nothing self.internal_uri.getAuthority + authority : Text | Nothing + authority self = self.internal_uri.getAuthority ## Get the port part of this URI. @@ -122,11 +112,10 @@ type URI import Standard.Examples example_port = Examples.uri.port - port : Text ! Nothing + port : Text | Nothing port self = port_number = self.internal_uri.getPort - handle_nothing <| - if port_number == -1 then Nothing else port_number.to_text + if port_number == -1 then Nothing else port_number.to_text ## GROUP Metadata Get the path part of this URI. @@ -137,8 +126,8 @@ type URI import Standard.Examples example_path = Examples.uri.path - path : Text ! Nothing - path self = handle_nothing self.internal_uri.getPath + path : Text | Nothing + path self = self.internal_uri.getPath ## GROUP Metadata Get the query part of this URI. @@ -149,8 +138,17 @@ type URI import Standard.Examples example_query = Examples.uri.query - query : Text ! Nothing - query self = handle_nothing self.internal_uri.getQuery + query : Text | Nothing + query self = self.internal_uri.getQuery + + ## Adds a query parameter to the URI + + Arguments: + - key: The key of the query parameter. + - value: The value of the query parameter. + add_query_argument : Text -> Text | Enso_Secret -> URI + add_query_argument self key:Text value:(Text|Enso_Secret) = + URI_With_Query.Value self [Pair.new key value] ## Get the fragment part of this URI. @@ -160,38 +158,38 @@ type URI import Standard.Examples example_fragment = Examples.uri.fragment - fragment : Text ! Nothing - fragment self = handle_nothing self.internal_uri.getFragment + fragment : Text | Nothing + fragment self = self.internal_uri.getFragment ## PRIVATE ADVANCED Get the unescaped user info part of this URI. - raw_user_info : Text ! Nothing - raw_user_info self = handle_nothing self.internal_uri.getRawUserInfo + raw_user_info : Text | Nothing + raw_user_info self = self.internal_uri.getRawUserInfo ## PRIVATE ADVANCED Get the unescaped authority part of this URI. - raw_authority : Text ! Nothing - raw_authority self = handle_nothing self.internal_uri.getRawAuthority + raw_authority : Text | Nothing + raw_authority self = self.internal_uri.getRawAuthority ## PRIVATE ADVANCED Get the unescaped path part of this URI. - raw_path : Text ! Nothing - raw_path self = handle_nothing self.internal_uri.getRawPath + raw_path : Text | Nothing + raw_path self = self.internal_uri.getRawPath ## PRIVATE ADVANCED Get the unescaped query part of this URI. - raw_query : Text ! Nothing - raw_query self = handle_nothing self.internal_uri.getRawQuery + raw_query : Text | Nothing + raw_query self = self.internal_uri.getRawQuery ## PRIVATE ADVANCED Get the unescaped fragment part of this URI. - raw_fragment : Text ! Nothing - raw_fragment self = handle_nothing self.internal_uri.getRawFragment + raw_fragment : Text | Nothing + raw_fragment self = self.internal_uri.getRawFragment ## PRIVATE Convert this URI to text. @@ -218,3 +216,6 @@ type URI type_pair = ["type", "URI"] cons_pair = ["constructor", "parse"] JS_Object.from_pairs [type_pair, cons_pair, ["uri", self.to_text]] + +## PRIVATE +URI.from (that:Text) = URI.parse that diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Network/URI_With_Query.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Network/URI_With_Query.enso new file mode 100644 index 0000000000..bd77ef5205 --- /dev/null +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Network/URI_With_Query.enso @@ -0,0 +1,112 @@ +import project.Data.Enso_Cloud.Enso_Secret.Enso_Secret +import project.Data.Enso_Cloud.Enso_Secret.Enso_Secret_Error +import project.Data.Json.JS_Object +import project.Data.Pair.Pair +import project.Data.Text.Encoding.Encoding +import project.Data.Text.Extensions +import project.Data.Text.Text +import project.Data.Vector.Vector +import project.Error.Error +import project.Errors.Common.Syntax_Error +import project.Meta +import project.Network.URI.URI +import project.Nothing.Nothing +import project.Panic.Panic +from project.Data.Boolean import Boolean, False, True + +polyglot java import java.lang.Exception +polyglot java import java.net.URI as Java_URI +polyglot java import org.enso.base.enso_cloud.EnsoSecretHelper + +## Represents a URI with a set of query parameters +type URI_With_Query + ## PRIVATE + Value uri:URI parameters:Vector + + ## PRIVATE + Convert this to URI. + Materialize the URI_With_Query into a URI, but will error if any secrets + are used. + to_uri : URI ! Enso_Secret_Error + to_uri self = + Panic.catch Exception (URI.Value (EnsoSecretHelper.replaceQuery self.uri.internal_uri self.query)) caught_panic-> + message = caught_panic.payload.getMessage + Error.throw (Syntax_Error.Error "Unable to collapse to a URI:"+message) + + ## GROUP Metadata + Get the query part of this URI, but will error if any secrets are used. + query : Text | Nothing ! Enso_Secret_Error + query self = + base_query = self.uri.query.if_nothing "" + query_params = self.parameters.map p-> + if p.second.is_a Enso_Secret then Error.throw Enso_Secret_Error.Access_Denied else + (EnsoSecretHelper.encodeArg p.first True) + "=" + (EnsoSecretHelper.encodeArg p.second False) + (if base_query == "" then "" else base_query + "&") + (query_params.join "&") + + ## Adds a query parameter to the URI + + Arguments: + - key: The key of the query parameter. + - value: The value of the query parameter. + add_query_argument : Text -> Text | Enso_Secret -> URI + add_query_argument self key:Text value:(Text|Enso_Secret) = + URI_With_Query.Value self.uri self.parameters+[Pair.new key value] + + ## PRIVATE + Convert this URI to text. + to_text : Text + to_text self = + base_query = self.uri.query.if_nothing "" + query_params = self.parameters.map p-> + if p.second.is_a Enso_Secret then p.first + "=__SECRET__" else + p.first + "=" + p.second + new_query = "?" + (if base_query == "" then "" else base_query + "&") + (query_params.join "&") + + Panic.catch Exception (EnsoSecretHelper.replaceQuery self.uri.internal_uri new_query . toString) caught_panic-> + message = caught_panic.payload.getMessage + Error.throw (Syntax_Error.Error "Unable to render URI_With_Query:"+message) + + ## PRIVATE + Convert to a display representation of this URI. + to_display_text : Text + to_display_text self = self.to_text.to_display_text + + ## PRIVATE + Convert to a JavaScript Object representing this URI. + to_js_object : JS_Object + to_js_object self = + type_pair = ["type", "URI_With_Query"] + cons_pair = ["constructor", "parse"] + JS_Object.from_pairs [type_pair, cons_pair, ["uri", self.uri.to_text], ["parameters", self.parameters.to_js_object]] + + ## GROUP Metadata + Get the scheme part of this URI. + scheme : Text | Nothing + scheme self = self.uri.scheme + + ## GROUP Metadata + Get the user info part of this URI. + user_info : Text | Nothing + user_info self = self.uri.user_info + + ## GROUP Metadata + Get the host part of this URI. + host : Text | Nothing + host self = self.uri.host + + ## Get the authority (user info and host) part of this URI. + authority : Text | Nothing + authority self = self.uri.authority + + ## Get the port part of this URI. + port : Text | Nothing + port self = self.uri.port + + ## GROUP Metadata + Get the path part of this URI. + path : Text | Nothing + path self = self.uri.path + + ## Get the fragment part of this URI. + fragment : Text | Nothing + fragment self = self.uri.fragment diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Random.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Random.enso index 1cecad8206..62fe64571a 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Random.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Random.enso @@ -1,14 +1,14 @@ import project.Any.Any import project.Data.Array.Array +import project.Data.Map.Map import project.Data.Range.Range +import project.Data.Set.Set import project.Data.Text.Text import project.Data.Time.Date.Date import project.Data.Time.Date_Range.Date_Range import project.Data.Time.Period.Period import project.Data.Time.Time_Of_Day.Time_Of_Day import project.Data.Time.Time_Period.Time_Period -import project.Data.Map.Map -import project.Data.Set.Set import project.Data.Vector.Vector import project.Error.Error import project.Errors.Illegal_Argument.Illegal_Argument @@ -44,8 +44,8 @@ type Random new_generator : Integer -> Random_Generator new_generator (seed:Integer|Nothing=Nothing) = Random_Generator.new seed - ## TEXT_ONLY - GROUP Random + ## GROUP Random + TEXT_ONLY Set the seed of the default `Random_Generator` instance. @@ -245,7 +245,7 @@ type Random permute : Vector Any -> Vector Any permute (v:Vector) = Random_Generator.global_random_generator.permute v -# PRIVATE +## PRIVATE type Random_Generator ## PRIVATE Create a new rng with the given seed. diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Runtime.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Runtime.enso index 946aefffac..fa80d51557 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Runtime.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Runtime.enso @@ -53,7 +53,8 @@ get_stack_trace = gc : Nothing gc = @Builtin_Method "Runtime.gc" -## ADVANCED +## PRIVATE + ADVANCED Asserts that the given action succeeds, otherwise throws a panic. @@ -66,7 +67,8 @@ assert ~action message="" = assert_builtin action message ## PRIVATE assert_builtin ~action message = @Builtin_Method "Runtime.assert_builtin" -## ADVANCED +## PRIVATE + ADVANCED Executes the provided action without allowing it to inline. diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/Runtime/Source_Location.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/Runtime/Source_Location.enso index e126550a2c..2032dffc80 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/Runtime/Source_Location.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/Runtime/Source_Location.enso @@ -1,5 +1,6 @@ import project.Data.Numbers.Integer import project.Data.Text.Text +import project.Nothing.Nothing import project.System.File.File from project.Data.Boolean import Boolean, False, True @@ -47,13 +48,21 @@ type Source_Location row + ":" + start + "-" + end False -> start_line.to_text + '-' + end_line.to_text - cwd = File.current_directory - file = self.file.absolute - formatted_file = case file.is_child_of cwd of - True -> cwd.relativize file . path - _ -> file.path - formatted_file + ":" + indices + if self.file.is_nothing then "? Unknown ?:" + indices else + cwd = File.current_directory + file = self.file.absolute + formatted_file = case file.is_child_of cwd of + True -> cwd.relativize file . path + _ -> file.path + formatted_file + ":" + indices ## Return the source file corresponding to this location. - file : File - file self = File.new self.prim_location.getSource.getPath + file : File | Nothing + file self = + source = self.prim_location.getSource + path = if source.is_nothing then Nothing else source.getPath + if path.is_nothing then Nothing else File.new path + + ## PRIVATE + to_display_text : Text + to_display_text self = self.formatted_coordinates diff --git a/distribution/lib/Standard/Base/0.0.0-dev/src/System/File_Format.enso b/distribution/lib/Standard/Base/0.0.0-dev/src/System/File_Format.enso index 60a62307c8..7645dd419c 100644 --- a/distribution/lib/Standard/Base/0.0.0-dev/src/System/File_Format.enso +++ b/distribution/lib/Standard/Base/0.0.0-dev/src/System/File_Format.enso @@ -54,8 +54,8 @@ type Auto_Detect Finds a matching format for reading the file. It assumes that `file` already exists. - get_reading_format : File -> Any | Nothing - get_reading_format file = + get_reading_format : File_For_Read -> Any | Nothing + get_reading_format file:File_For_Read = get_format f-> f.for_file_read file ## PRIVATE @@ -121,8 +121,8 @@ type Plain_Text_Format ## PRIVATE If the File_Format supports reading from the file, return a configured instance. - for_file_read : File -> Plain_Text_Format | Nothing - for_file_read file = + for_file_read : File_For_Read -> Plain_Text_Format | Nothing + for_file_read file:File_For_Read = case file.extension of ".txt" -> Plain_Text_Format.Plain_Text ".log" -> Plain_Text_Format.Plain_Text @@ -164,8 +164,8 @@ type Plain_Text_Format type Bytes ## PRIVATE If the File_Format supports reading from the file, return a configured instance. - for_file_read : File -> Bytes | Nothing - for_file_read file = + for_file_read : File_For_Read -> Bytes | Nothing + for_file_read file:File_For_Read = case file.extension of ".dat" -> Bytes _ -> Nothing @@ -200,8 +200,8 @@ type Bytes type JSON_Format ## PRIVATE If the File_Format supports reading from the file, return a configured instance. - for_file_read : File -> JSON_Format | Nothing - for_file_read file = + for_file_read : File_For_Read -> JSON_Format | Nothing + for_file_read file:File_For_Read = case file.extension of ".json" -> JSON_Format ".geojson" -> JSON_Format @@ -239,3 +239,17 @@ type JSON_Format ## A setting to infer the default behaviour of some option. type Infer + +## PRIVATE + A minimal interface allowing for_file_read +type File_For_Read + ## PRIVATE + Arguments: + - `path` - the path or the URI of the file. + - `name` - the name of the file. + - `extension` - the extension of the file. + - `read_first_bytes` - a function that reads the first bytes of the file. + Value path:Text name:Text extension:Text (read_first_bytes:Function=(_->Nothing)) + +## PRIVATE +File_For_Read.from (that:File) = File_For_Read.Value that.path that.name that.extension that.read_first_bytes diff --git a/distribution/lib/Standard/Database/0.0.0-dev/src/Connection/Credentials.enso b/distribution/lib/Standard/Database/0.0.0-dev/src/Connection/Credentials.enso index f111dff2e1..f0463d3a31 100644 --- a/distribution/lib/Standard/Database/0.0.0-dev/src/Connection/Credentials.enso +++ b/distribution/lib/Standard/Database/0.0.0-dev/src/Connection/Credentials.enso @@ -2,7 +2,7 @@ from Standard.Base import all type Credentials ## Simple username and password type. - Username_And_Password username:Text password:Text + Username_And_Password username:Text password:(Text|Enso_Secret) ## PRIVATE Override `to_text` to mask the password field. diff --git a/distribution/lib/Standard/Database/0.0.0-dev/src/Connection/SQLite_Format.enso b/distribution/lib/Standard/Database/0.0.0-dev/src/Connection/SQLite_Format.enso index a3bea690cf..0f8f44308d 100644 --- a/distribution/lib/Standard/Database/0.0.0-dev/src/Connection/SQLite_Format.enso +++ b/distribution/lib/Standard/Database/0.0.0-dev/src/Connection/SQLite_Format.enso @@ -1,5 +1,6 @@ from Standard.Base import all import Standard.Base.Errors.Illegal_Argument.Illegal_Argument +import Standard.Base.System.File_Format.File_For_Read import project.Connection.Database import project.Connection.SQLite_Details.SQLite_Details @@ -11,11 +12,16 @@ type SQLite_Format ## PRIVATE If the File_Format supports reading from the file, return a configured instance. - for_file_read : File -> SQLite_Format | Nothing - for_file_read file = + for_file_read : File_For_Read -> SQLite_Format | Nothing + for_file_read file:File_For_Read = expected_header = magic_header_string got_header = file.read_first_bytes expected_header.length - if got_header == expected_header then SQLite_Format.For_File else Nothing + if got_header == expected_header then SQLite_Format.For_File else + ## To allow for reading a SQLite file by extension if we cannot read the file header. + if got_header.is_nothing.not then Nothing else case file.extension of + ".db" -> SQLite_Format.For_File + ".sqlite" -> SQLite_Format.For_File + _ -> Nothing ## PRIVATE If the File_Format supports writing to the file, return a configured instance. diff --git a/distribution/lib/Standard/Database/0.0.0-dev/src/Data/Column.enso b/distribution/lib/Standard/Database/0.0.0-dev/src/Data/Column.enso index 8845402668..e2b1f176a1 100644 --- a/distribution/lib/Standard/Database/0.0.0-dev/src/Data/Column.enso +++ b/distribution/lib/Standard/Database/0.0.0-dev/src/Data/Column.enso @@ -1780,3 +1780,9 @@ simple_unary_op column op_kind = column.make_unary_op op_kind Materialized_Column.from (that:Column) = _ = [that] Error.throw (Illegal_Argument.Error "Currently cross-backend operations are not supported. Materialize the column using `.read` before mixing it with an in-memory Table.") + +## PRIVATE + Conversion method to a Column to a Vector. +Vector.from (that:Column) = + _ = [that] + Error.throw (Illegal_Argument.Error "To convert from a Database column to a vector you must first call `read` on the column.") diff --git a/distribution/lib/Standard/Database/0.0.0-dev/src/Data/Table.enso b/distribution/lib/Standard/Database/0.0.0-dev/src/Data/Table.enso index 2d8c3338a1..6a90203be3 100644 --- a/distribution/lib/Standard/Database/0.0.0-dev/src/Data/Table.enso +++ b/distribution/lib/Standard/Database/0.0.0-dev/src/Data/Table.enso @@ -13,10 +13,10 @@ from Standard.Base.Metadata import make_single_choice from Standard.Base.Runtime import assert from Standard.Base.Widget_Helpers import make_delimiter_selector +import Standard.Table.Data.Blank_Selector.Blank_Selector import Standard.Table.Data.Calculations.Column_Operation.Column_Operation import Standard.Table.Data.Column_Ref.Column_Ref import Standard.Table.Data.Constants.Previous_Value -import Standard.Table.Data.Blank_Selector.Blank_Selector import Standard.Table.Data.Expression.Expression import Standard.Table.Data.Expression.Expression_Error import Standard.Table.Data.Join_Condition.Join_Condition @@ -618,7 +618,7 @@ type Table column = self.evaluate_expression expression on_problems self.filter column Filter_Condition.Is_True - ## ALIAS first, last, slice, sample + ## ALIAS first, last, sample, slice GROUP Standard.Base.Selections Creates a new Table with the specified range of rows from the input Table. @@ -928,7 +928,7 @@ type Table make_temp_column_name : Text make_temp_column_name self = self.column_naming_helper.make_temp_column_name self.column_names - ## PRIVSATE + ## PRIVATE Run a table transformer with a temporary column added. with_temporary_column : Column -> (Text -> Table -> Table) -> Table with_temporary_column self new_column:Column f:(Text -> Table -> Table) = @@ -2095,36 +2095,36 @@ type Table Error.throw (Unsupported_Database_Operation.Error "Table.expand_column is currently not implemented for the Database backend. You may download the table to memory using `.read` to use this feature.") ## GROUP Standard.Base.Conversions - Expand aggregate values in a column to separate rows. + Expand aggregate values in a column to separate rows. - For each value in the specified column, if it is an aggregate (`Vector`, - `Range`, etc.), expand it to multiple rows, duplicating the values in the - other columns. + For each value in the specified column, if it is an aggregate (`Vector`, + `Range`, etc.), expand it to multiple rows, duplicating the values in the + other columns. - Arguments: - - column: The column to expand. - - at_least_one_row: for an empty aggregate value, if `at_least_one_row` is - true, a single row is output with `Nothing` for the aggregates column; if - false, no row is output at all. + Arguments: + - column: The column to expand. + - at_least_one_row: for an empty aggregate value, if `at_least_one_row` is + true, a single row is output with `Nothing` for the aggregates column; if + false, no row is output at all. - The following aggregate values are supported: - - `Array` - - `Vector` - - `List` - - `Range` - - `Date_Range` - - `Pair + The following aggregate values are supported: + - `Array` + - `Vector` + - `List` + - `Range` + - `Date_Range` + - `Pair - Any other values are treated as non-aggregate values, and their rows are kept - unchanged. + Any other values are treated as non-aggregate values, and their rows are kept + unchanged. - In in-memory tables, it is permitted to mix values of different types. + In in-memory tables, it is permitted to mix values of different types. - > Example - Expand a column of integer `Vectors` to a column of `Integer` + > Example + Expand a column of integer `Vectors` to a column of `Integer` - table = Table.new [["aaa", [1, 2]], ["bbb", [[30, 31], [40, 41]]]] - # => Table.new [["aaa", [1, 1, 2, 2]], ["bbb", [30, 31, 40, 41]]] + table = Table.new [["aaa", [1, 2]], ["bbb", [[30, 31], [40, 41]]]] + # => Table.new [["aaa", [1, 1, 2, 2]], ["bbb", [30, 31, 40, 41]]] @column Widget_Helpers.make_column_name_selector expand_to_rows : Text | Integer -> Boolean -> Table ! Type_Error | No_Such_Column | Index_Out_Of_Bounds expand_to_rows self column at_least_one_row=False = diff --git a/distribution/lib/Standard/Database/0.0.0-dev/src/Internal/JDBC_Connection.enso b/distribution/lib/Standard/Database/0.0.0-dev/src/Internal/JDBC_Connection.enso index 4ded729749..993411214d 100644 --- a/distribution/lib/Standard/Database/0.0.0-dev/src/Internal/JDBC_Connection.enso +++ b/distribution/lib/Standard/Database/0.0.0-dev/src/Internal/JDBC_Connection.enso @@ -23,7 +23,7 @@ polyglot java import java.sql.DatabaseMetaData polyglot java import java.sql.PreparedStatement polyglot java import java.sql.SQLException polyglot java import java.sql.SQLTimeoutException -polyglot java import java.util.Properties +polyglot java import org.enso.base.enso_cloud.EnsoKeyValuePair polyglot java import org.enso.database.dryrun.OperationSynchronizer polyglot java import org.enso.database.JDBCProxy @@ -259,11 +259,10 @@ type JDBC_Connection - properties: A vector of properties for the connection. create : Text -> Vector -> JDBC_Connection create url properties = handle_sql_errors <| - java_props = Properties.new - properties.each pair-> - case pair.second of - Nothing -> Polyglot.invoke java_props "remove" [pair.first] - _ -> Polyglot.invoke java_props "setProperty" [pair.first, pair.second] + java_props = properties.map pair-> case pair.second of + Nothing -> EnsoKeyValuePair.ofNothing pair.first + _ : Enso_Secret -> EnsoKeyValuePair.ofSecret pair.first pair.second.id + _ : Text -> EnsoKeyValuePair.ofText pair.first pair.second java_connection = JDBCProxy.getConnection url java_props resource = Managed_Resource.register java_connection close_connection diff --git a/distribution/lib/Standard/Database/0.0.0-dev/src/Internal/SQLite/SQLite_Entity_Naming_Properties.enso b/distribution/lib/Standard/Database/0.0.0-dev/src/Internal/SQLite/SQLite_Entity_Naming_Properties.enso index 0fc0039d86..8256aee13f 100644 --- a/distribution/lib/Standard/Database/0.0.0-dev/src/Internal/SQLite/SQLite_Entity_Naming_Properties.enso +++ b/distribution/lib/Standard/Database/0.0.0-dev/src/Internal/SQLite/SQLite_Entity_Naming_Properties.enso @@ -1,7 +1,7 @@ from Standard.Base import all -import Standard.Table.Internal.Naming_Properties.Unlimited_Naming_Properties import Standard.Table.Internal.Naming_Properties.Enso_Length_Limited_Naming_Properties +import Standard.Table.Internal.Naming_Properties.Unlimited_Naming_Properties import project.Internal.Connection.Entity_Naming_Properties.Entity_Naming_Properties diff --git a/distribution/lib/Standard/Image/0.0.0-dev/src/Image_File_Format.enso b/distribution/lib/Standard/Image/0.0.0-dev/src/Image_File_Format.enso index b239c4c3ed..09901c59b9 100644 --- a/distribution/lib/Standard/Image/0.0.0-dev/src/Image_File_Format.enso +++ b/distribution/lib/Standard/Image/0.0.0-dev/src/Image_File_Format.enso @@ -1,5 +1,6 @@ from Standard.Base import all import Standard.Base.Errors.Illegal_Argument.Illegal_Argument +import Standard.Base.System.File_Format.File_For_Read import project.Data.Image.Image @@ -14,8 +15,8 @@ type Image_File_Format ## PRIVATE If the File_Format supports reading from the file, return a configured instance. - for_file_read : File -> Image_File_Format | Nothing - for_file_read file = + for_file_read : File_For_Read -> Image_File_Format | Nothing + for_file_read file:File_For_Read = extension = file.extension if supported.contains extension then Image_File_Format.For_File else Nothing diff --git a/distribution/lib/Standard/Table/0.0.0-dev/src/Data/Blank_Selector.enso b/distribution/lib/Standard/Table/0.0.0-dev/src/Data/Blank_Selector.enso index 25db6abff4..4de1d247a2 100644 --- a/distribution/lib/Standard/Table/0.0.0-dev/src/Data/Blank_Selector.enso +++ b/distribution/lib/Standard/Table/0.0.0-dev/src/Data/Blank_Selector.enso @@ -3,4 +3,4 @@ type Blank_Selector ## Blank_Selector is used as a constructor for other functions. Any_Cell_Blank - All_Cells_Blank \ No newline at end of file + All_Cells_Blank diff --git a/distribution/lib/Standard/Table/0.0.0-dev/src/Data/Column.enso b/distribution/lib/Standard/Table/0.0.0-dev/src/Data/Column.enso index afeeebaa61..709917ad9a 100644 --- a/distribution/lib/Standard/Table/0.0.0-dev/src/Data/Column.enso +++ b/distribution/lib/Standard/Table/0.0.0-dev/src/Data/Column.enso @@ -2557,6 +2557,10 @@ fill_previous column is_missing = Conversion method to a Column from a Vector. Column.from (that:Vector) (name:Text="Vector") = Column.from_vector name that +## PRIVATE + Conversion method to a Column to a Vector. +Vector.from (that:Column) = that.to_vector + ## PRIVATE Conversion method to a Column from a Vector. Column.from (that:Range) (name:Text="Range") = Column.from_vector name that.to_vector diff --git a/distribution/lib/Standard/Table/0.0.0-dev/src/Data/Data_Formatter.enso b/distribution/lib/Standard/Table/0.0.0-dev/src/Data/Data_Formatter.enso index cc1e167149..8067f5f981 100644 --- a/distribution/lib/Standard/Table/0.0.0-dev/src/Data/Data_Formatter.enso +++ b/distribution/lib/Standard/Table/0.0.0-dev/src/Data/Data_Formatter.enso @@ -25,10 +25,10 @@ polyglot java import org.enso.table.parsing.DateParser polyglot java import org.enso.table.parsing.DateTimeParser polyglot java import org.enso.table.parsing.IdentityParser polyglot java import org.enso.table.parsing.NumberParser +polyglot java import org.enso.table.parsing.problems.ParseProblemAggregator polyglot java import org.enso.table.parsing.TimeOfDayParser polyglot java import org.enso.table.parsing.TypeInferringParser polyglot java import org.enso.table.parsing.WhitespaceStrippingParser -polyglot java import org.enso.table.parsing.problems.ParseProblemAggregator type Data_Formatter ## Specifies options for reading text data in a table to more specific types and diff --git a/distribution/lib/Standard/Table/0.0.0-dev/src/Data/Table.enso b/distribution/lib/Standard/Table/0.0.0-dev/src/Data/Table.enso index 4ded4753af..b2d818fbd6 100644 --- a/distribution/lib/Standard/Table/0.0.0-dev/src/Data/Table.enso +++ b/distribution/lib/Standard/Table/0.0.0-dev/src/Data/Table.enso @@ -15,8 +15,8 @@ from Standard.Base.Metadata import make_single_choice from Standard.Base.Widget_Helpers import make_delimiter_selector import project.Data.Aggregate_Column.Aggregate_Column -import project.Data.Calculations.Column_Operation.Column_Operation import project.Data.Blank_Selector.Blank_Selector +import project.Data.Calculations.Column_Operation.Column_Operation import project.Data.Column as Column_Module import project.Data.Column.Column import project.Data.Column_Ref.Column_Ref @@ -33,9 +33,9 @@ import project.Data.Report_Unmatched.Report_Unmatched import project.Data.Row.Row import project.Data.Set_Mode.Set_Mode import project.Data.Sort_Column.Sort_Column -import project.Extensions.Table_Conversions import project.Delimited.Delimited_Format.Delimited_Format import project.Extensions.Prefix_Name.Prefix_Name +import project.Extensions.Table_Conversions import project.Extensions.Table_Ref.Table_Ref import project.Internal.Add_Row_Number import project.Internal.Aggregate_Column_Helper @@ -54,11 +54,9 @@ import project.Internal.Split_Tokenize import project.Internal.Table_Helpers import project.Internal.Table_Helpers.Table_Column_Helper import project.Internal.Widget_Helpers - from project.Data.Column import get_item_string, normalize_string_for_display from project.Data.Type.Value_Type import Auto, Value_Type from project.Errors import all -#from project.Extensions.Table_Conversions import all from project.Internal.Filter_Condition_Helpers import make_filter_column from project.Internal.Lookup_Helpers import make_java_lookup_column_description from project.Internal.Rows_View import Rows_View @@ -73,10 +71,10 @@ polyglot java import org.enso.table.data.table.join.conditions.Equals as Java_Jo polyglot java import org.enso.table.data.table.join.conditions.EqualsIgnoreCase as Java_Join_Equals_Ignore_Case polyglot java import org.enso.table.data.table.join.lookup.LookupJoin polyglot java import org.enso.table.data.table.Table as Java_Table -polyglot java import org.enso.table.error.TooManyColumnsException -polyglot java import org.enso.table.error.NullValuesInKeyColumns -polyglot java import org.enso.table.error.UnmatchedRow polyglot java import org.enso.table.error.NonUniqueLookupKey +polyglot java import org.enso.table.error.NullValuesInKeyColumns +polyglot java import org.enso.table.error.TooManyColumnsException +polyglot java import org.enso.table.error.UnmatchedRow polyglot java import org.enso.table.operations.OrderBuilder polyglot java import org.enso.table.parsing.problems.ParseProblemAggregator @@ -1287,36 +1285,36 @@ type Table Expand_Objects_Helpers.expand_column self column fields prefix ## GROUP Standard.Base.Conversions - Expand aggregate values in a column to separate rows. + Expand aggregate values in a column to separate rows. - For each value in the specified column, if it is an aggregate (`Vector`, - `Range`, etc.), expand it to multiple rows, duplicating the values in the - other columns. + For each value in the specified column, if it is an aggregate (`Vector`, + `Range`, etc.), expand it to multiple rows, duplicating the values in the + other columns. - Arguments: - - column: The column to expand. - - at_least_one_row: for an empty aggregate value, if `at_least_one_row` is - true, a single row is output with `Nothing` for the aggregates column; if - false, no row is output at all. + Arguments: + - column: The column to expand. + - at_least_one_row: for an empty aggregate value, if `at_least_one_row` is + true, a single row is output with `Nothing` for the aggregates column; if + false, no row is output at all. - The following aggregate values are supported: - - `Array` - - `Vector` - - `List` - - `Range` - - `Date_Range` - - `Pair + The following aggregate values are supported: + - `Array` + - `Vector` + - `List` + - `Range` + - `Date_Range` + - `Pair - Any other values are treated as non-aggregate values, and their rows are kept - unchanged. + Any other values are treated as non-aggregate values, and their rows are kept + unchanged. - In in-memory tables, it is permitted to mix values of different types. + In in-memory tables, it is permitted to mix values of different types. - > Example - Expand a column of integer `Vectors` to a column of `Integer` + > Example + Expand a column of integer `Vectors` to a column of `Integer` - table = Table.new [["aaa", [1, 2]], ["bbb", [[30, 31], [40, 41]]]] - # => Table.new [["aaa", [1, 1, 2, 2]], ["bbb", [30, 31, 40, 41]]] + table = Table.new [["aaa", [1, 2]], ["bbb", [[30, 31], [40, 41]]]] + # => Table.new [["aaa", [1, 1, 2, 2]], ["bbb", [30, 31, 40, 41]]] @column Widget_Helpers.make_column_name_selector expand_to_rows : Text | Integer -> Boolean -> Table ! Type_Error | No_Such_Column | Index_Out_Of_Bounds expand_to_rows self column at_least_one_row=False = @@ -1423,7 +1421,7 @@ type Table column = self.evaluate_expression expression on_problems self.filter column Filter_Condition.Is_True - ## ALIAS first, last, slice, sample + ## ALIAS first, last, sample, slice GROUP Standard.Base.Selections Creates a new Table with the specified range of rows from the input Table. @@ -2662,7 +2660,8 @@ concat_columns column_set all_tables result_type result_row_count on_problems = Conversion method to a Table from a Column. Table.from (that:Column) = that.to_table -## Converts a Text value into a Table. +## PRIVATE + Converts a Text value into a Table. The format of the text is determined by the `format` argument. @@ -2675,7 +2674,8 @@ Table.from (that : Text) (format:Delimited_Format = Delimited_Format.Delimited ' _ : Delimited_Format -> Delimited_Reader.read_text that format on_problems _ -> Unimplemented.throw "Table.from is currently only implemented for Delimited_Format." -## Converts a Table into a Text value. +## PRIVATE + Converts a Table into a Text value. The format of the text is determined by the `format` argument. @@ -2691,7 +2691,8 @@ Text.from (that : Table) (format:Delimited_Format = Delimited_Format.Delimited ' Conversion method to a Table from a Vector. Table.from (that:Vector) (fields : (Vector | Nothing) = Nothing) = that.to_table fields -## Convert an `XML_Element` into a `Table` +## PRIVATE + Convert an `XML_Element` into a `Table` Generates a single-row table with columns for the tag's contents. @@ -2711,7 +2712,8 @@ Table.from (that:XML_Element) = # Generating a table from an `XML_Element` is the same logic as `expand_column` Table.new [["x", [that]]] . expand_column 'x' prefix=Prefix_Name.None -## Convert an `XML_Document` into a `Table` +## PRIVATE + Convert an `XML_Document` into a `Table` Generates a single-row table with columns for the root tag's contents. diff --git a/distribution/lib/Standard/Table/0.0.0-dev/src/Delimited/Delimited_Format.enso b/distribution/lib/Standard/Table/0.0.0-dev/src/Delimited/Delimited_Format.enso index 576a9ba71b..01b5423a44 100644 --- a/distribution/lib/Standard/Table/0.0.0-dev/src/Delimited/Delimited_Format.enso +++ b/distribution/lib/Standard/Table/0.0.0-dev/src/Delimited/Delimited_Format.enso @@ -1,5 +1,6 @@ from Standard.Base import all import Standard.Base.Network.HTTP.Response.Response +import Standard.Base.System.File_Format.File_For_Read import Standard.Base.System.Input_Stream.Input_Stream from Standard.Base.Widget_Helpers import make_delimiter_selector @@ -58,8 +59,8 @@ type Delimited_Format ## PRIVATE ADVANCED If the File_Format supports reading from the file, return a configured instance. - for_file_read : File -> Delimited_Format | Nothing - for_file_read file = + for_file_read : File_For_Read -> Delimited_Format | Nothing + for_file_read file:File_For_Read = case file.extension of ".csv" -> Delimited_Format.Delimited ',' ".tab" -> Delimited_Format.Delimited '\t' diff --git a/distribution/lib/Standard/Table/0.0.0-dev/src/Excel/Excel_Format.enso b/distribution/lib/Standard/Table/0.0.0-dev/src/Excel/Excel_Format.enso index 0ab6acb7d1..69967c5219 100644 --- a/distribution/lib/Standard/Table/0.0.0-dev/src/Excel/Excel_Format.enso +++ b/distribution/lib/Standard/Table/0.0.0-dev/src/Excel/Excel_Format.enso @@ -1,5 +1,6 @@ from Standard.Base import all import Standard.Base.Errors.Illegal_Argument.Illegal_Argument +import Standard.Base.System.File_Format.File_For_Read import Standard.Base.System.Input_Stream.Input_Stream import project.Data.Match_Columns.Match_Columns @@ -49,8 +50,8 @@ type Excel_Format ## PRIVATE ADVANCED If the File_Format supports reading from the file, return a configured instance. - for_file_read : File -> Excel_Format | Nothing - for_file_read file = + for_file_read : File_For_Read -> Excel_Format | Nothing + for_file_read file:File_For_Read = is_xls = should_treat_as_xls_format Infer file if is_xls.is_error then Nothing else Excel_Format.Excel xls_format=is_xls diff --git a/distribution/lib/Standard/Table/0.0.0-dev/src/Excel/Excel_Workbook.enso b/distribution/lib/Standard/Table/0.0.0-dev/src/Excel/Excel_Workbook.enso index e905e59f1f..83294b3cd2 100644 --- a/distribution/lib/Standard/Table/0.0.0-dev/src/Excel/Excel_Workbook.enso +++ b/distribution/lib/Standard/Table/0.0.0-dev/src/Excel/Excel_Workbook.enso @@ -207,6 +207,7 @@ type Excel_Workbook Arguments: - name: the name of the worksheet to read. + @name (self-> Single_Choice display=Display.Always values=(self.sheet_names.map t-> Option t t.pretty)) sheet : Text | Integer -> Table sheet self name:(Text|Integer) = self.read_section (Excel_Section.Worksheet name 0 Nothing) diff --git a/distribution/lib/Standard/Table/0.0.0-dev/src/Extensions/Prefix_Name.enso b/distribution/lib/Standard/Table/0.0.0-dev/src/Extensions/Prefix_Name.enso index f090f4ee68..afcdd5fcc0 100644 --- a/distribution/lib/Standard/Table/0.0.0-dev/src/Extensions/Prefix_Name.enso +++ b/distribution/lib/Standard/Table/0.0.0-dev/src/Extensions/Prefix_Name.enso @@ -1,8 +1,14 @@ from Standard.Base import all type Prefix_Name - None - Column_Name - Custom value:Text + ## Do not add a prefix to the column name. + None -Prefix_Name.from (that:Text) = Prefix_Name.Custom that \ No newline at end of file + ## Use the column name as a prefix. + Column_Name + + ## Add a custom prefix to the new name. + Custom value:Text + +## PRIVATE +Prefix_Name.from (that:Text) = Prefix_Name.Custom that diff --git a/distribution/lib/Standard/Table/0.0.0-dev/src/Extensions/Table_Ref.enso b/distribution/lib/Standard/Table/0.0.0-dev/src/Extensions/Table_Ref.enso index 1fc007d107..f8effd0672 100644 --- a/distribution/lib/Standard/Table/0.0.0-dev/src/Extensions/Table_Ref.enso +++ b/distribution/lib/Standard/Table/0.0.0-dev/src/Extensions/Table_Ref.enso @@ -8,7 +8,6 @@ import project.Data.Set_Mode.Set_Mode import project.Data.Table.Table from project.Errors import No_Such_Column, Existing_Column, Missing_Column - ## PRIVATE A helper type allowing to resolve column references in a context of an underlying table. type Table_Ref diff --git a/distribution/lib/Standard/Table/0.0.0-dev/src/Internal/Column_Naming_Helper.enso b/distribution/lib/Standard/Table/0.0.0-dev/src/Internal/Column_Naming_Helper.enso index c6db169b02..7beb43215a 100644 --- a/distribution/lib/Standard/Table/0.0.0-dev/src/Internal/Column_Naming_Helper.enso +++ b/distribution/lib/Standard/Table/0.0.0-dev/src/Internal/Column_Naming_Helper.enso @@ -1,8 +1,8 @@ from Standard.Base import all import Standard.Base.Errors.Illegal_Argument.Illegal_Argument -import project.Internal.Naming_Properties.Unlimited_Naming_Properties import project.Internal.Naming_Properties.Enso_Length_Limited_Naming_Properties +import project.Internal.Naming_Properties.Unlimited_Naming_Properties import project.Internal.Unique_Name_Strategy.Unique_Name_Strategy from project.Errors import Clashing_Column_Name, Invalid_Column_Names from project.Internal.Table_Helpers import is_column diff --git a/distribution/lib/Standard/Table/0.0.0-dev/src/Internal/Expand_Objects_Helpers.enso b/distribution/lib/Standard/Table/0.0.0-dev/src/Internal/Expand_Objects_Helpers.enso index 548f575060..0616171a76 100644 --- a/distribution/lib/Standard/Table/0.0.0-dev/src/Internal/Expand_Objects_Helpers.enso +++ b/distribution/lib/Standard/Table/0.0.0-dev/src/Internal/Expand_Objects_Helpers.enso @@ -1,13 +1,12 @@ from Standard.Base import all - import Standard.Base.Errors.Common.Index_Out_Of_Bounds import Standard.Base.Errors.Common.Type_Error import Standard.Base.Errors.Illegal_Argument.Illegal_Argument -import project.Data.Table.Table import project.Data.Column.Column import project.Data.Conversions.Convertible_To_Columns.Convertible_To_Columns import project.Data.Conversions.Convertible_To_Rows.Convertible_To_Rows +import project.Data.Table.Table import project.Errors.No_Such_Column import project.Extensions.Prefix_Name.Prefix_Name import project.Internal.Fan_Out @@ -41,7 +40,8 @@ expand_column (table : Table) (column : Text | Integer) (fields : (Vector Text) Table.new output_builder.to_vector -## GROUP Standard.Base.Conversions +## PRIVATE + GROUP Standard.Base.Conversions Expand aggregate values in a column to separate rows. For each value in the specified column, if it is an aggregate (`Vector`, diff --git a/distribution/lib/Standard/Table/0.0.0-dev/src/Main.enso b/distribution/lib/Standard/Table/0.0.0-dev/src/Main.enso index 486b2b102c..52e2f85d11 100644 --- a/distribution/lib/Standard/Table/0.0.0-dev/src/Main.enso +++ b/distribution/lib/Standard/Table/0.0.0-dev/src/Main.enso @@ -1,8 +1,8 @@ from Standard.Base import all import project.Data.Aggregate_Column.Aggregate_Column -import project.Data.Calculations.Column_Operation.Column_Operation import project.Data.Blank_Selector.Blank_Selector +import project.Data.Calculations.Column_Operation.Column_Operation import project.Data.Column.Column import project.Data.Column_Ref.Column_Ref import project.Data.Column_Vector_Extensions @@ -31,8 +31,9 @@ from project.Excel.Excel_Section.Excel_Section import Cell_Range, Range_Names, S from project.Extensions.Table_Conversions import all export project.Data.Aggregate_Column.Aggregate_Column -export project.Data.Calculations.Column_Operation.Column_Operation + export project.Data.Blank_Selector.Blank_Selector +export project.Data.Calculations.Column_Operation.Column_Operation export project.Data.Column.Column export project.Data.Column_Ref.Column_Ref export project.Data.Column_Vector_Extensions @@ -59,3 +60,4 @@ from project.Delimited.Delimited_Format.Delimited_Format export Delimited from project.Excel.Excel_Format.Excel_Format export Excel from project.Excel.Excel_Section.Excel_Section export Cell_Range, Range_Names, Sheet_Names, Worksheet from project.Extensions.Table_Conversions export all + diff --git a/project/DistributionPackage.scala b/project/DistributionPackage.scala index a0164f6445..14b51b6d73 100644 --- a/project/DistributionPackage.scala +++ b/project/DistributionPackage.scala @@ -95,6 +95,9 @@ object DistributionPackage { def executableName(baseName: String): String = if (Platform.isWindows) baseName + ".exe" else baseName + private def batName(baseName: String): String = + if (Platform.isWindows) baseName + ".bat" else baseName + def createProjectManagerPackage( distributionRoot: File, cacheFactory: CacheStoreFactory @@ -266,7 +269,7 @@ object DistributionPackage { ): Boolean = { import scala.collection.JavaConverters._ - val enso = distributionRoot / "bin" / "enso" + val enso = distributionRoot / "bin" / batName("enso") log.info(s"Executing $enso ${args.mkString(" ")}") val pb = new java.lang.ProcessBuilder() val all = new java.util.ArrayList[String]() diff --git a/std-bits/base/src/main/java/org/enso/base/Http_Utils.java b/std-bits/base/src/main/java/org/enso/base/Http_Utils.java deleted file mode 100644 index 1a81dc1ead..0000000000 --- a/std-bits/base/src/main/java/org/enso/base/Http_Utils.java +++ /dev/null @@ -1,54 +0,0 @@ -package org.enso.base; - -import java.net.http.HttpHeaders; -import java.util.AbstractMap; -import java.util.List; -import java.util.Map; -import org.enso.base.net.http.BasicAuthorization; -import org.enso.base.net.http.MultipartBodyBuilder; -import org.enso.base.net.http.UrlencodedBodyBuilder; - -/** Utils for standard HTTP library. */ -public class Http_Utils { - - /** - * Create the header for HTTP basic auth. - * - * @param user the user name. - * @param password the password. - * @return the new header. - */ - public static String header_basic_auth(String user, String password) { - return BasicAuthorization.header(user, password); - } - - /** - * Create the builder for a multipart form data. - * - * @return the multipart form builder. - */ - public static MultipartBodyBuilder multipart_body_builder() { - return new MultipartBodyBuilder(); - } - - /** - * Create the builder for an url-encoded form data. - * - * @return the url-encoded form builder. - */ - public static UrlencodedBodyBuilder urlencoded_body_builder() { - return new UrlencodedBodyBuilder(); - } - /** - * Get HTTP response headers as a list of map entries. - * - * @param headers HTTP response headers. - * @return the key-value list of headers. - */ - public static Object[] get_headers(HttpHeaders headers) { - Map> map = headers.map(); - return map.keySet().stream() - .flatMap(k -> map.get(k).stream().map(v -> new AbstractMap.SimpleImmutableEntry<>(k, v))) - .toArray(); - } -} diff --git a/std-bits/base/src/main/java/org/enso/base/enso_cloud/AuthenticationProvider.java b/std-bits/base/src/main/java/org/enso/base/enso_cloud/AuthenticationProvider.java new file mode 100644 index 0000000000..347d163d0a --- /dev/null +++ b/std-bits/base/src/main/java/org/enso/base/enso_cloud/AuthenticationProvider.java @@ -0,0 +1,23 @@ +package org.enso.base.enso_cloud; + +public class AuthenticationProvider { + private static String token; + + public static String setToken(String token) { + AuthenticationProvider.token = token; + return AuthenticationProvider.token; + } + + public static String getToken() { + return AuthenticationProvider.token; + } + + public static String getAPIRootURI() { + var envUri = System.getenv("ENSO_CLOUD_API_URI"); + return envUri == null ? "https://7aqkn3tnbc.execute-api.eu-west-1.amazonaws.com/" : envUri; + } + + public static void flushCloudCaches() { + EnsoSecretReader.flushCache(); + } +} diff --git a/std-bits/base/src/main/java/org/enso/base/enso_cloud/EnsoKeyValuePair.java b/std-bits/base/src/main/java/org/enso/base/enso_cloud/EnsoKeyValuePair.java new file mode 100644 index 0000000000..d02e7de191 --- /dev/null +++ b/std-bits/base/src/main/java/org/enso/base/enso_cloud/EnsoKeyValuePair.java @@ -0,0 +1,21 @@ +package org.enso.base.enso_cloud; + +record EnsoKeySecretPair(String key, String secretId) implements EnsoKeyValuePair {} + +record EnsoKeyStringPair(String key, String value) implements EnsoKeyValuePair {} + +public sealed interface EnsoKeyValuePair permits EnsoKeySecretPair, EnsoKeyStringPair { + String key(); + + static EnsoKeyValuePair ofNothing(String key) { + return new EnsoKeyStringPair(key, null); + } + + static EnsoKeyValuePair ofText(String key, String value) { + return new EnsoKeyStringPair(key, value); + } + + static EnsoKeyValuePair ofSecret(String key, String secretId) { + return new EnsoKeySecretPair(key, secretId); + } +} diff --git a/std-bits/base/src/main/java/org/enso/base/enso_cloud/EnsoSecretHelper.java b/std-bits/base/src/main/java/org/enso/base/enso_cloud/EnsoSecretHelper.java new file mode 100644 index 0000000000..348751978c --- /dev/null +++ b/std-bits/base/src/main/java/org/enso/base/enso_cloud/EnsoSecretHelper.java @@ -0,0 +1,144 @@ +package org.enso.base.enso_cloud; + +import java.io.IOException; +import java.io.InputStream; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.http.HttpClient; +import java.net.http.HttpHeaders; +import java.net.http.HttpRequest.Builder; +import java.net.http.HttpResponse; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.SQLException; +import java.util.List; +import java.util.Properties; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * Makes HTTP requests with secrets in either header or query string. + */ +public class EnsoSecretHelper { + /** + * Gets the value of an EnsoKeyValuePair resolving secrets. + * @param pair The pair to resolve. + * @return The pair's value. Should not be returned to Enso. + */ + private static String resolveValue(EnsoKeyValuePair pair) { + return switch (pair) { + case EnsoKeyStringPair stringPair -> stringPair.value(); + case EnsoKeySecretPair secretPair -> + EnsoSecretReader.readSecret(secretPair.secretId()); + case null -> + throw new IllegalArgumentException("EnsoKeyValuePair should not be NULL."); + }; + } + + /** + * Converts an EnsoKeyValuePair into a string for display purposes. Does not include secrets. + * @param pair The pair to render. + * @return The rendered string. + */ + private static String renderValue(EnsoKeyValuePair pair) { + return switch (pair) { + case EnsoKeyStringPair stringPair -> stringPair.value(); + case EnsoKeySecretPair _ -> "__SECRET__"; + case null -> + throw new IllegalArgumentException("EnsoKeyValuePair should not be NULL."); + }; + } + + /** + * Substitutes the minimal parts within the string for the URI parse. + * */ + public static String encodeArg(String arg, boolean includeEquals) { + var encoded = arg.replace("%", "%25") + .replace("&", "%26") + .replace(" ", "%20"); + if (includeEquals) { + encoded = encoded.replace("=", "%3D"); + } + return encoded; + } + + /** + * Replaces the query string in a URI. + * */ + public static URI replaceQuery(URI uri, String newQuery) throws URISyntaxException { + var baseURI = new URI(uri.getScheme(), uri.getAuthority(), uri.getPath(), null, null).toString(); + + var baseFragment = uri.getFragment(); + baseFragment = baseFragment != null && !baseFragment.isBlank() ? "#" + baseFragment : ""; + + return URI.create(baseURI + newQuery + baseFragment); + } + + private static String makeQueryAry(EnsoKeyValuePair pair, Function resolver) { + String resolvedKey = pair.key() != null && !pair.key().isBlank() ? encodeArg(pair.key(), true) + "=" : ""; + String resolvedValue = encodeArg(resolver.apply(pair), false); + return resolvedKey + resolvedValue; + } + + //** Gets a JDBC connection resolving EnsoKeyValuePair into the properties. **// + public static Connection getJDBCConnection(String url, EnsoKeyValuePair[] properties) + throws SQLException { + var javaProperties = new Properties(); + for (EnsoKeyValuePair pair : properties) { + javaProperties.setProperty(pair.key(), resolveValue(pair)); + } + + return DriverManager.getConnection(url, javaProperties); + } + + //** Makes a request with secrets in the query string or headers. **// + public static EnsoHttpResponse makeRequest(HttpClient client, Builder builder, URI uri, List queryArguments, List headerArguments) + throws IOException, InterruptedException { + + // Build a new URI with the query arguments. + URI resolvedURI = uri; + URI renderedURI = uri; + if (queryArguments != null && !queryArguments.isEmpty()) { + try { + var baseQuery = uri.getQuery(); + baseQuery = baseQuery != null && !baseQuery.isBlank() ? "?" + baseQuery + "&" : "?"; + var query = baseQuery + queryArguments.stream().map(p -> makeQueryAry(p, EnsoSecretHelper::resolveValue)).collect(Collectors.joining("&")); + + resolvedURI = replaceQuery(uri, query); + renderedURI = resolvedURI; + if (queryArguments.stream().anyMatch(p -> p instanceof EnsoKeySecretPair)) { + if (!resolvedURI.getScheme().equals("https")) { + // If used a secret then only allow HTTPS + throw new IllegalArgumentException("Cannot use secrets in query string with non-HTTPS URI."); + } + + var renderedQuery = baseQuery + queryArguments.stream().map(p -> makeQueryAry(p, EnsoSecretHelper::renderValue)).collect(Collectors.joining("&")); + renderedURI = replaceQuery(uri, renderedQuery); + } + } catch (URISyntaxException e) { + throw new IllegalArgumentException("Unable to build a valid URI."); + } + } + builder.uri(resolvedURI); + + // Resolve the header arguments. + if (headerArguments != null && !headerArguments.isEmpty()) { + for (EnsoKeyValuePair header : headerArguments) { + builder.header(header.key(), resolveValue(header)); + } + } + + // Build and Send the request. + var httpRequest = builder.build(); + var bodyHandler = HttpResponse.BodyHandlers.ofInputStream(); + var javaResponse = client.send(httpRequest, bodyHandler); + + // Extract parts of the response + return new EnsoHttpResponse(renderedURI, javaResponse.headers().map().keySet().stream().toList(), javaResponse.headers(), javaResponse.body(), javaResponse.statusCode()); + } + + /** + * A subset of the HttpResponse to avoid leaking the decrypted Enso secrets. + */ + public record EnsoHttpResponse(URI uri, List headerNames, HttpHeaders headers, InputStream body, int statusCode) { } +} diff --git a/std-bits/base/src/main/java/org/enso/base/enso_cloud/EnsoSecretReader.java b/std-bits/base/src/main/java/org/enso/base/enso_cloud/EnsoSecretReader.java new file mode 100644 index 0000000000..82823b4036 --- /dev/null +++ b/std-bits/base/src/main/java/org/enso/base/enso_cloud/EnsoSecretReader.java @@ -0,0 +1,60 @@ +package org.enso.base.enso_cloud; + +import java.io.IOException; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse; +import java.util.HashMap; +import java.util.Map; + +/** * Internal class to read secrets from the Enso Cloud. */ +class EnsoSecretReader { + private static final Map secrets = new HashMap<>(); + + static void flushCache() { + secrets.clear(); + } + + /** + * * Reads a secret from the Enso Cloud. + * + * @param secretId the ID of the secret to read. + * @return the secret value. + */ + static String readSecret(String secretId) { + if (secrets.containsKey(secretId)) { + return secrets.get(secretId); + } + + var apiUri = AuthenticationProvider.getAPIRootURI() + "/secrets/" + secretId; + var client = HttpClient.newBuilder().followRedirects(HttpClient.Redirect.ALWAYS).build(); + var request = + HttpRequest.newBuilder() + .uri(URI.create(apiUri)) + .header("Authorization", "Bearer " + AuthenticationProvider.getToken()) + .GET() + .build(); + + HttpResponse response; + + try { + response = client.send(request, HttpResponse.BodyHandlers.ofString()); + } catch (IOException | InterruptedException e) { + throw new IllegalArgumentException("Unable to read secret."); + } + + if (response.statusCode() != 200) { + throw new IllegalArgumentException("Unable to read secret."); + } + + var secretJSON = response.body(); + var secretValue = readValueFromString(secretJSON); + secrets.put(secretId, secretValue); + return secretValue; + } + + private static String readValueFromString(String json) { + return json.substring(1, json.length() - 1).translateEscapes(); + } +} diff --git a/std-bits/base/src/main/java/org/enso/base/net/http/BasicAuthorization.java b/std-bits/base/src/main/java/org/enso/base/net/http/BasicAuthorization.java deleted file mode 100644 index 8a9ebb627c..0000000000 --- a/std-bits/base/src/main/java/org/enso/base/net/http/BasicAuthorization.java +++ /dev/null @@ -1,21 +0,0 @@ -package org.enso.base.net.http; - -import java.nio.charset.StandardCharsets; -import java.util.Base64; - -/** An authenticator for HTTP basic auth. */ -public final class BasicAuthorization { - - /** - * Build HTTP basic authorization header. - * - * @param user the user name. - * @param password the password - * @return return base64 encoded header for HTTP basic auth. - */ - public static String header(String user, String password) { - String auth = user + ":" + password; - String authEncoded = Base64.getEncoder().encodeToString(auth.getBytes(StandardCharsets.UTF_8)); - return "Basic " + authEncoded; - } -} diff --git a/std-bits/database/src/main/java/org/enso/database/JDBCProxy.java b/std-bits/database/src/main/java/org/enso/database/JDBCProxy.java index c7c78828cd..79c6bdf6d9 100644 --- a/std-bits/database/src/main/java/org/enso/database/JDBCProxy.java +++ b/std-bits/database/src/main/java/org/enso/database/JDBCProxy.java @@ -3,8 +3,9 @@ package org.enso.database; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; -import java.util.Properties; import java.util.ServiceLoader; +import org.enso.base.enso_cloud.EnsoKeyValuePair; +import org.enso.base.enso_cloud.EnsoSecretHelper; /** * A helper class for accessing the JDBC components. @@ -35,7 +36,8 @@ public class JDBCProxy { * @param properties configuration for the connection * @return a connection */ - public static Connection getConnection(String url, Properties properties) throws SQLException { + public static Connection getConnection(String url, EnsoKeyValuePair[] properties) + throws SQLException { // We need to manually register all the drivers because the DriverManager is not able // to correctly use our class loader, it only delegates to the platform class loader when // loading the java.sql.Driver service. @@ -43,6 +45,7 @@ public class JDBCProxy { for (var driver : sl) { DriverManager.registerDriver(driver); } - return DriverManager.getConnection(url, properties); + + return EnsoSecretHelper.getJDBCConnection(url, properties); } } diff --git a/test/Tests/src/Data/Vector_Spec.enso b/test/Tests/src/Data/Vector_Spec.enso index 66bf5ff55d..bb73149edc 100644 --- a/test/Tests/src/Data/Vector_Spec.enso +++ b/test/Tests/src/Data/Vector_Spec.enso @@ -387,7 +387,7 @@ type_spec name alter = Test.group name <| Test.specify "should define concatenation" <| concat = (alter [1, 2, 3]) + (alter [4, 5, 6]) concat.should_equal [1, 2, 3, 4, 5, 6] - (alter [1, 2, 3])+1 . should_fail_with Type_Error + Test.expect_panic_with matcher=Type_Error ((alter [1, 2, 3])+1) Test.specify "should allow finding a value" <| input = alter [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] diff --git a/test/Tests/src/Network/Http_Spec.enso b/test/Tests/src/Network/Http_Spec.enso index 72610a812b..993d26c5a5 100644 --- a/test/Tests/src/Network/Http_Spec.enso +++ b/test/Tests/src/Network/Http_Spec.enso @@ -78,7 +78,10 @@ spec = expected_response = Json.parse <| ''' { "headers": { + "Connection": "Upgrade, HTTP2-Settings", + "Http2-Settings": "AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA", "User-Agent": "Java-http-client/21.0.1", + "Upgrade": "h2c", "Content-Length": "0" }, "origin": "127.0.0.1", @@ -119,7 +122,10 @@ spec = expected_response = Json.parse <| ''' { "headers": { + "Connection": "Upgrade, HTTP2-Settings", + "Http2-Settings": "AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA", "User-Agent": "Java-http-client/21.0.1", + "Upgrade": "h2c", "Content-Length": "0" }, "origin": "127.0.0.1", @@ -166,7 +172,10 @@ spec = expected_response = Json.parse <| ''' { "headers": { + "Connection": "Upgrade, HTTP2-Settings", + "Http2-Settings": "AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA", "User-Agent": "Java-http-client/21.0.1", + "Upgrade": "h2c", "Content-Type": "text/plain; charset=UTF-8", "Content-Length": "11" }, @@ -189,7 +198,10 @@ spec = expected_response = Json.parse <| ''' { "headers": { + "Connection": "Upgrade, HTTP2-Settings", + "Http2-Settings": "AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA", "User-Agent": "Java-http-client/21.0.1", + "Upgrade": "h2c", "Content-Type": "application/json", "Content-Length": "20" }, @@ -209,7 +221,10 @@ spec = expected_response = Json.parse <| ''' { "headers": { + "Connection": "Upgrade, HTTP2-Settings", + "Http2-Settings": "AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA", "User-Agent": "Java-http-client/21.0.1", + "Upgrade": "h2c", "Content-Type": "application/json", "Content-Length": "20" }, @@ -228,7 +243,10 @@ spec = expected_response = Json.parse <| ''' { "headers": { + "Connection": "Upgrade, HTTP2-Settings", + "Http2-Settings": "AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA", "User-Agent": "Java-http-client/21.0.1", + "Upgrade": "h2c", "Content-Type": "application/json", "Content-Length": "50" }, @@ -247,7 +265,10 @@ spec = expected_response = Json.parse <| ''' { "headers": { + "Connection": "Upgrade, HTTP2-Settings", + "Http2-Settings": "AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA", "User-Agent": "Java-http-client/21.0.1", + "Upgrade": "h2c", "Content-Type": "application/json", "Content-Length": "47" }, @@ -270,7 +291,10 @@ spec = expected_response = Json.parse <| ''' { "headers": { + "Connection": "Upgrade, HTTP2-Settings", + "Http2-Settings": "AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA", "User-Agent": "Java-http-client/21.0.1", + "Upgrade": "h2c", "Content-Type": "text/plain; charset=UTF-16LE", "Content-Length": "24" }, @@ -289,7 +313,10 @@ spec = expected_response = Json.parse <| ''' { "headers": { + "Connection": "Upgrade, HTTP2-Settings", + "Http2-Settings": "AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA", "User-Agent": "Java-http-client/21.0.1", + "Upgrade": "h2c", "Content-Type": "text/csv; charset=UTF-8", "Content-Length": "6" }, @@ -343,7 +370,10 @@ spec = expected_response = Json.parse <| ''' { "headers": { + "Connection": "Upgrade, HTTP2-Settings", + "Http2-Settings": "AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA", "User-Agent": "Java-http-client/21.0.1", + "Upgrade": "h2c", "Content-Type": "text/plain; charset=UTF-8", "Content-Length": "11" }, @@ -362,7 +392,10 @@ spec = expected_response = Json.parse <| ''' { "headers": { + "Connection": "Upgrade, HTTP2-Settings", + "Http2-Settings": "AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA", "User-Agent": "Java-http-client/21.0.1", + "Upgrade": "h2c", "Content-Type": "text/plain; charset=UTF-8", "Content-Length": "11" }, @@ -381,7 +414,10 @@ spec = expected_response = Json.parse <| ''' { "headers": { + "Connection": "Upgrade, HTTP2-Settings", + "Http2-Settings": "AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA", "User-Agent": "Java-http-client/21.0.1", + "Upgrade": "h2c", "Content-Type": "application/diff; charset=UTF-8", "Content-Length": "11" }, @@ -400,7 +436,10 @@ spec = expected_response = Json.parse <| ''' { "headers": { + "Connection": "Upgrade, HTTP2-Settings", + "Http2-Settings": "AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA", "User-Agent": "Java-http-client/21.0.1", + "Upgrade": "h2c", "Content-Length": "0" }, "origin": "127.0.0.1", @@ -418,7 +457,10 @@ spec = expected_response = Json.parse <| ''' { "headers": { + "Connection": "Upgrade, HTTP2-Settings", + "Http2-Settings": "AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA", "User-Agent": "Java-http-client/21.0.1", + "Upgrade": "h2c", "Content-Type": "text/plain; charset=UTF-8", "Content-Length": "11" }, @@ -437,7 +479,10 @@ spec = expected_response = Json.parse <| ''' { "headers": { + "Connection": "Upgrade, HTTP2-Settings", + "Http2-Settings": "AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA", "User-Agent": "Java-http-client/21.0.1", + "Upgrade": "h2c", "Content-Type": "text/plain; charset=UTF-8", "Content-Length": "11", "Custom": "asdf" @@ -483,7 +528,10 @@ spec = expected_response = Json.parse <| ''' { "headers": { + "Connection": "Upgrade, HTTP2-Settings", + "Http2-Settings": "AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA", "User-Agent": "Java-http-client/21.0.1", + "Upgrade": "h2c", "Content-Type": "application/json; charset=UTF-8", "Content-Length": "23" }, @@ -502,7 +550,10 @@ spec = expected_response = Json.parse <| ''' { "headers": { + "Connection": "Upgrade, HTTP2-Settings", + "Http2-Settings": "AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA", "User-Agent": "Java-http-client/21.0.1", + "Upgrade": "h2c", "Content-Type": "application/json", "Content-Length": "23" }, @@ -521,7 +572,10 @@ spec = expected_response = Json.parse <| ''' { "headers": { + "Connection": "Upgrade, HTTP2-Settings", + "Http2-Settings": "AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA", "User-Agent": "Java-http-client/21.0.1", + "Upgrade": "h2c", "Content-Type": "application/json", "Content-Length": "23" }, @@ -540,7 +594,10 @@ spec = expected_response = Json.parse <| ''' { "headers": { + "Connection": "Upgrade, HTTP2-Settings", + "Http2-Settings": "AAEAAEAAAAIAAAABAAMAAABkAAQBAAAAAAUAAEAA", "User-Agent": "Java-http-client/21.0.1", + "Upgrade": "h2c", "Content-Type": "text/plain; charset=UTF-8", "Content-Length": "23" }, diff --git a/test/Tests/src/Network/URI_Spec.enso b/test/Tests/src/Network/URI_Spec.enso index b73853a5c0..f1cdc3820e 100644 --- a/test/Tests/src/Network/URI_Spec.enso +++ b/test/Tests/src/Network/URI_Spec.enso @@ -12,22 +12,22 @@ spec = addr.user_info.should_equal "user:pass" addr.host.should_equal "example.com" addr.authority.should_equal "user:pass@example.com" - addr.port.should_fail_with Nothing + addr.port.should_equal Nothing addr.path.should_equal "/foo/bar" addr.query.should_equal "key=val" - addr.fragment.should_fail_with Nothing + addr.fragment.should_equal Nothing Test.specify "should escape URI" <| addr = URI.parse "https://%D0%9B%D0%B8%D0%BD%D1%83%D1%81:pass@ru.wikipedia.org/wiki/%D0%AF%D0%B4%D1%80%D0%BE_Linux?%D0%9A%D0%BE%D0%B4" addr.user_info.should_equal "Линус:pass" addr.authority.should_equal "Линус:pass@ru.wikipedia.org" addr.path.should_equal "/wiki/Ядро_Linux" addr.query.should_equal "Код" - addr.fragment.should_fail_with Nothing + addr.fragment.should_equal Nothing addr.raw_user_info.should_equal "%D0%9B%D0%B8%D0%BD%D1%83%D1%81:pass" addr.raw_authority.should_equal "%D0%9B%D0%B8%D0%BD%D1%83%D1%81:pass@ru.wikipedia.org" addr.raw_path.should_equal "/wiki/%D0%AF%D0%B4%D1%80%D0%BE_Linux" addr.raw_query.should_equal "%D0%9A%D0%BE%D0%B4" - addr.raw_fragment.should_fail_with Nothing + addr.raw_fragment.should_equal Nothing Test.specify "should return Syntax_Error when parsing invalid URI" <| URI.parse "a b c" . should_fail_with Syntax_Error