mirror of
https://github.com/enso-org/enso.git
synced 2024-11-22 11:52:59 +03:00
Integrating Enso Cloud with the libraries (part 1...) (#8006)
- Add a `File_For_Read` type. Used for `File_Format` to read files. - Added `Enso_User` representing the current user in `Enso_Cloud`. - *Will be later able to list known users.* - Added `Enso_Secret` representing a value defined in `Enso_Cloud`. - Value not used within Enso only accessed within polyglot Java. - Integrated into `Username_And_Password` and can be used within JDBC connections. - Integrated into HTTP Headers so a secret can be used as a value. - New `URI_With_Query` with the same API as `URI`. Supporting secrets in the value. - *Will be integrated with AWS credentials.* - Added `Enso_File` representing a file or a folder in the cloud. - Support the same API as `File` (like the `S3_File`). - *Will support `enso://` URI style access.*
This commit is contained in:
parent
1138dfe147
commit
ecaca12df1
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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`.
|
||||
|
@ -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
|
@ -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."
|
@ -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
|
@ -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."
|
@ -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) _->
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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`.
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
|
@ -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
|
@ -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.
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
@ -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.
|
||||
|
@ -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.")
|
||||
|
@ -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 =
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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.
|
||||
|
||||
|
@ -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'
|
||||
|
@ -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
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
||||
## 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
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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`,
|
||||
|
@ -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
|
||||
|
||||
|
@ -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]()
|
||||
|
@ -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<String, List<String>> map = headers.map();
|
||||
return map.keySet().stream()
|
||||
.flatMap(k -> map.get(k).stream().map(v -> new AbstractMap.SimpleImmutableEntry<>(k, v)))
|
||||
.toArray();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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<EnsoKeyValuePair, String> 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<EnsoKeyValuePair> queryArguments, List<EnsoKeyValuePair> 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<String> headerNames, HttpHeaders headers, InputStream body, int statusCode) { }
|
||||
}
|
@ -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<String, String> 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<String> 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();
|
||||
}
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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]
|
||||
|
@ -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"
|
||||
},
|
||||
|
@ -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
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user