feat: Add Python SDK capabilities to generate Polars and DuckDB connection settings to S3 (#2625)

* feat: Add Python SDK capabilities to generate Polars and DuckDB connection settings to S3

* Add endpoint to list S3 objects

* Add FE to set the workspace S3 resource

* Fix openapi

* sqlx prepare

* Hide Windmil LFS tab
This commit is contained in:
Guillaume Bouvignies
2023-11-15 19:37:02 +01:00
committed by GitHub
parent b649fae0b0
commit f932e4c916
29 changed files with 2102 additions and 663 deletions

View File

@@ -42,7 +42,6 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",

View File

@@ -82,6 +82,11 @@
"ordinal": 15,
"name": "error_handler_muted_on_cancel",
"type_info": "Bool"
},
{
"ordinal": 16,
"name": "large_file_storage",
"type_info": "Jsonb"
}
],
"parameters": {
@@ -105,7 +110,8 @@
true,
false,
true,
false
false,
true
]
},
"hash": "1730f39fd1793d45fbb41b21389c61296a3ff7489ae12f52a19f9543173ac597"

View File

@@ -67,7 +67,6 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",

View File

@@ -0,0 +1,14 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE workspace_settings SET large_file_storage = NULL WHERE workspace_id = $1",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Text"
]
},
"nullable": []
},
"hash": "2ff397ad0aeabdc15776025f75c2d1390ce5ac700b0bf71481d31747c8f2e870"
}

View File

@@ -28,7 +28,6 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",

View File

@@ -82,6 +82,11 @@
"ordinal": 15,
"name": "error_handler_muted_on_cancel",
"type_info": "Bool"
},
{
"ordinal": 16,
"name": "large_file_storage",
"type_info": "Jsonb"
}
],
"parameters": {
@@ -105,7 +110,8 @@
true,
false,
true,
false
false,
true
]
},
"hash": "5445083864b2b092b012e894bff7630a1d7b9deb8d33e9f909061f351f96844e"

View File

@@ -0,0 +1,22 @@
{
"db_name": "PostgreSQL",
"query": "SELECT large_file_storage FROM workspace_settings WHERE workspace_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "large_file_storage",
"type_info": "Jsonb"
}
],
"parameters": {
"Left": [
"Text"
]
},
"nullable": [
true
]
},
"hash": "5e4e0740e1cff4b2a7c620da3758604a47f81b505fa9a713efa3519b050b546a"
}

View File

@@ -60,7 +60,6 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",

View File

@@ -40,7 +40,6 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",

View File

@@ -46,7 +46,6 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",

View File

@@ -42,7 +42,6 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",

View File

@@ -0,0 +1,15 @@
{
"db_name": "PostgreSQL",
"query": "UPDATE workspace_settings SET large_file_storage = $1 WHERE workspace_id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Jsonb",
"Text"
]
},
"nullable": []
},
"hash": "bf27083af3514e5fae868364518096d1cf4a042c23526dc134c3b2d27904e954"
}

View File

@@ -42,7 +42,6 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",

View File

@@ -37,7 +37,6 @@
"bash",
"postgresql",
"nativets",
"Nativets",
"bun",
"mysql",
"bigquery",

511
backend/Cargo.lock generated
View File

@@ -503,6 +503,382 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa"
[[package]]
name = "aws-config"
version = "0.57.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2bf00cb9416daab4ce4927c54ebe63c08b9caf4d7b9314b6d7a4a2c5a1afb09"
dependencies = [
"aws-credential-types",
"aws-http",
"aws-runtime",
"aws-sdk-sso",
"aws-sdk-ssooidc",
"aws-sdk-sts",
"aws-smithy-async",
"aws-smithy-http",
"aws-smithy-json",
"aws-smithy-runtime",
"aws-smithy-runtime-api",
"aws-smithy-types",
"aws-types",
"bytes",
"fastrand 2.0.1",
"hex",
"http",
"hyper",
"ring 0.17.5",
"time",
"tokio",
"tracing",
"zeroize",
]
[[package]]
name = "aws-credential-types"
version = "0.57.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb9073c88dbf12f68ce7d0e149f989627a1d1ae3d2b680459f04ccc29d1cbd0f"
dependencies = [
"aws-smithy-async",
"aws-smithy-runtime-api",
"aws-smithy-types",
"zeroize",
]
[[package]]
name = "aws-http"
version = "0.57.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24067106d09620cf02d088166cdaedeaca7146d4d499c41b37accecbea11b246"
dependencies = [
"aws-smithy-http",
"aws-smithy-runtime-api",
"aws-smithy-types",
"aws-types",
"bytes",
"http",
"http-body",
"pin-project-lite",
"tracing",
]
[[package]]
name = "aws-runtime"
version = "0.57.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc6ee0152c06d073602236a4e94a8c52a327d310c1ecd596570ce795af8777ff"
dependencies = [
"aws-credential-types",
"aws-http",
"aws-sigv4",
"aws-smithy-async",
"aws-smithy-eventstream",
"aws-smithy-http",
"aws-smithy-runtime-api",
"aws-smithy-types",
"aws-types",
"fastrand 2.0.1",
"http",
"percent-encoding",
"tracing",
"uuid 1.5.0",
]
[[package]]
name = "aws-sdk-s3"
version = "0.36.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf188e8af89adb0b41ae2f21b6c666c618959e40242e79aaa43fafa9c0c90a8a"
dependencies = [
"aws-credential-types",
"aws-http",
"aws-runtime",
"aws-sigv4",
"aws-smithy-async",
"aws-smithy-checksums",
"aws-smithy-eventstream",
"aws-smithy-http",
"aws-smithy-json",
"aws-smithy-runtime",
"aws-smithy-runtime-api",
"aws-smithy-types",
"aws-smithy-xml",
"aws-types",
"bytes",
"http",
"http-body",
"once_cell",
"percent-encoding",
"regex",
"tracing",
"url",
]
[[package]]
name = "aws-sdk-sso"
version = "0.36.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2eb8158015232b4596ccef74a205600398e152d704b40b7ec9f486092474d7fa"
dependencies = [
"aws-credential-types",
"aws-http",
"aws-runtime",
"aws-smithy-async",
"aws-smithy-http",
"aws-smithy-json",
"aws-smithy-runtime",
"aws-smithy-runtime-api",
"aws-smithy-types",
"aws-types",
"bytes",
"http",
"regex",
"tracing",
]
[[package]]
name = "aws-sdk-ssooidc"
version = "0.36.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36a1493e1c57f173e53621935bfb5b6217376168dbdb4cd459aebcf645924a48"
dependencies = [
"aws-credential-types",
"aws-http",
"aws-runtime",
"aws-smithy-async",
"aws-smithy-http",
"aws-smithy-json",
"aws-smithy-runtime",
"aws-smithy-runtime-api",
"aws-smithy-types",
"aws-types",
"bytes",
"http",
"regex",
"tracing",
]
[[package]]
name = "aws-sdk-sts"
version = "0.36.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e032b77f5cd1dd3669d777a38ac08cbf8ec68e29460d4ef5d3e50cffa74ec75a"
dependencies = [
"aws-credential-types",
"aws-http",
"aws-runtime",
"aws-smithy-async",
"aws-smithy-http",
"aws-smithy-json",
"aws-smithy-query",
"aws-smithy-runtime",
"aws-smithy-runtime-api",
"aws-smithy-types",
"aws-smithy-xml",
"aws-types",
"http",
"regex",
"tracing",
]
[[package]]
name = "aws-sigv4"
version = "0.57.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64f81a6abc4daab06b53cabf27c54189928893283093e37164ca53aa47488a5b"
dependencies = [
"aws-credential-types",
"aws-smithy-eventstream",
"aws-smithy-http",
"aws-smithy-runtime-api",
"bytes",
"form_urlencoded",
"hex",
"hmac",
"http",
"num-bigint",
"once_cell",
"p256",
"percent-encoding",
"regex",
"ring 0.17.5",
"sha2 0.10.8",
"time",
"tracing",
"zeroize",
]
[[package]]
name = "aws-smithy-async"
version = "0.57.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dbe53fccd3b10414b9cae63767a15a2789b34e6c6727b6e32b33e8c7998a3e80"
dependencies = [
"futures-util",
"pin-project-lite",
"tokio",
]
[[package]]
name = "aws-smithy-checksums"
version = "0.57.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fb5701fbfb40600cc0fa547f318552dfd4e632b2099bd75d95fb0faae70675d"
dependencies = [
"aws-smithy-http",
"aws-smithy-types",
"bytes",
"crc32c",
"crc32fast",
"hex",
"http",
"http-body",
"md-5 0.10.6",
"pin-project-lite",
"sha1",
"sha2 0.10.8",
"tracing",
]
[[package]]
name = "aws-smithy-eventstream"
version = "0.57.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b33fa99f928a5815b94ee07e1377901bcf51aa749034a2c802dc38f9dcfacf5"
dependencies = [
"aws-smithy-types",
"bytes",
"crc32fast",
]
[[package]]
name = "aws-smithy-http"
version = "0.57.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7972373213d1d6e619c0edc9dda2d6634154e4ed75c5e0b2bf065cd5ec9f0d1"
dependencies = [
"aws-smithy-eventstream",
"aws-smithy-runtime-api",
"aws-smithy-types",
"bytes",
"bytes-utils",
"futures-core",
"http",
"http-body",
"once_cell",
"percent-encoding",
"pin-project-lite",
"pin-utils",
"tracing",
]
[[package]]
name = "aws-smithy-json"
version = "0.57.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6d64d5af16dd585de9ff6c606423c1aaad47c6baa38de41c2beb32ef21c6645"
dependencies = [
"aws-smithy-types",
]
[[package]]
name = "aws-smithy-query"
version = "0.57.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7527bf5335154ba1b285479c50b630e44e93d1b4a759eaceb8d0bf9fbc82caa5"
dependencies = [
"aws-smithy-types",
"urlencoding",
]
[[package]]
name = "aws-smithy-runtime"
version = "0.57.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "839b363adf3b2bdab2742a1f540fec23039ea8bc9ec0f9f61df48470cfe5527b"
dependencies = [
"aws-smithy-async",
"aws-smithy-http",
"aws-smithy-runtime-api",
"aws-smithy-types",
"bytes",
"fastrand 2.0.1",
"http",
"http-body",
"hyper",
"hyper-rustls",
"once_cell",
"pin-project-lite",
"pin-utils",
"rustls 0.21.8",
"tokio",
"tracing",
]
[[package]]
name = "aws-smithy-runtime-api"
version = "0.57.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f24ecc446e62c3924539e7c18dec8038dba4fdf8718d5c2de62f9d2fecca8ba9"
dependencies = [
"aws-smithy-async",
"aws-smithy-types",
"bytes",
"http",
"pin-project-lite",
"tokio",
"tracing",
"zeroize",
]
[[package]]
name = "aws-smithy-types"
version = "0.57.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "051de910296522a21178a2ea402ea59027eef4b63f1cef04a0be2bb5e25dea03"
dependencies = [
"base64-simd",
"bytes",
"bytes-utils",
"futures-core",
"http",
"http-body",
"itoa",
"num-integer",
"pin-project-lite",
"pin-utils",
"ryu",
"serde",
"time",
"tokio",
"tokio-util",
]
[[package]]
name = "aws-smithy-xml"
version = "0.57.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb1e3ac22c652662096c8e37a6f9af80c6f3520cab5610b2fe76c725bce18eac"
dependencies = [
"xmlparser",
]
[[package]]
name = "aws-types"
version = "0.57.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "048bbf1c24cdf4eb1efcdc243388a93a90ebf63979e25fc1c7b8cbd9cb6beb38"
dependencies = [
"aws-credential-types",
"aws-smithy-async",
"aws-smithy-runtime-api",
"aws-smithy-types",
"http",
"rustc_version 0.4.0",
"tracing",
]
[[package]]
name = "axum"
version = "0.6.20"
@@ -568,6 +944,12 @@ dependencies = [
"rustc-demangle",
]
[[package]]
name = "base16ct"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce"
[[package]]
name = "base64"
version = "0.13.1"
@@ -911,6 +1293,16 @@ version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223"
[[package]]
name = "bytes-utils"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e47d3a8076e283f3acd27400535992edb3ba4b5bb72f8891ad8fbe7932a7d4b9"
dependencies = [
"bytes",
"either",
]
[[package]]
name = "bzip2"
version = "0.4.4"
@@ -1312,6 +1704,15 @@ version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
[[package]]
name = "crc32c"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8f48d60e5b4d2c53d5c2b1d8a58c849a70ae5e5509b08a48d047e3b65714a74"
dependencies = [
"rustc_version 0.4.0",
]
[[package]]
name = "crc32fast"
version = "1.3.2"
@@ -1405,6 +1806,18 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7"
[[package]]
name = "crypto-bigint"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef"
dependencies = [
"generic-array",
"rand_core 0.6.4",
"subtle",
"zeroize",
]
[[package]]
name = "crypto-common"
version = "0.1.6"
@@ -1949,6 +2362,18 @@ dependencies = [
"reborrow",
]
[[package]]
name = "ecdsa"
version = "0.14.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c"
dependencies = [
"der 0.6.1",
"elliptic-curve",
"rfc6979",
"signature 1.6.4",
]
[[package]]
name = "either"
version = "1.9.0"
@@ -1958,6 +2383,26 @@ dependencies = [
"serde",
]
[[package]]
name = "elliptic-curve"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3"
dependencies = [
"base16ct",
"crypto-bigint",
"der 0.6.1",
"digest 0.10.7",
"ff",
"generic-array",
"group",
"pkcs8 0.9.0",
"rand_core 0.6.4",
"sec1",
"subtle",
"zeroize",
]
[[package]]
name = "embed-doc-image"
version = "0.1.4"
@@ -2153,6 +2598,16 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
[[package]]
name = "ff"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160"
dependencies = [
"rand_core 0.6.4",
"subtle",
]
[[package]]
name = "filetime"
version = "0.2.22"
@@ -2706,6 +3161,17 @@ dependencies = [
"unic-ucd-category",
]
[[package]]
name = "group"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7"
dependencies = [
"ff",
"rand_core 0.6.4",
"subtle",
]
[[package]]
name = "h2"
version = "0.3.21"
@@ -2959,6 +3425,7 @@ dependencies = [
"futures-util",
"http",
"hyper",
"log",
"rustls 0.21.8",
"rustls-native-certs",
"tokio",
@@ -4010,6 +4477,17 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "p256"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594"
dependencies = [
"ecdsa",
"elliptic-curve",
"sha2 0.10.8",
]
[[package]]
name = "parking"
version = "2.2.0"
@@ -5093,6 +5571,17 @@ dependencies = [
"winreg",
]
[[package]]
name = "rfc6979"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb"
dependencies = [
"crypto-bigint",
"hmac",
"zeroize",
]
[[package]]
name = "riff"
version = "1.0.1"
@@ -5572,6 +6061,20 @@ version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "sec1"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928"
dependencies = [
"base16ct",
"der 0.6.1",
"generic-array",
"pkcs8 0.9.0",
"subtle",
"zeroize",
]
[[package]]
name = "security-framework"
version = "2.9.2"
@@ -8221,6 +8724,8 @@ dependencies = [
"async-recursion",
"async-stripe",
"async_zip",
"aws-config",
"aws-sdk-s3",
"axum",
"base64 0.21.5",
"bytes",
@@ -8727,6 +9232,12 @@ dependencies = [
"libc",
]
[[package]]
name = "xmlparser"
version = "0.13.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4"
[[package]]
name = "xz2"
version = "0.1.7"

View File

@@ -0,0 +1,2 @@
-- Add down migration script here
ALTER TABLE workspace_settings DROP COLUMN large_file_storage;

View File

@@ -0,0 +1,2 @@
-- Add up migration script here
ALTER TABLE workspace_settings ADD COLUMN IF NOT EXISTS large_file_storage JSONB;

View File

@@ -81,4 +81,6 @@ hf-hub.workspace = true
tokenizers.workspace = true
candle-core.workspace = true
candle-transformers.workspace = true
candle-nn.workspace = true
candle-nn.workspace = true
aws-sdk-s3 = "0.36.0"
aws-config = "0.57.2"

File diff suppressed because it is too large Load Diff

View File

@@ -1176,6 +1176,8 @@ paths:
$ref: "#/components/schemas/ScriptArgs"
error_handler_muted_on_cancel:
type: boolean
large_file_storage:
$ref: "#/components/schemas/LargeFileStorage"
required:
- code_completion_enabled
@@ -1469,6 +1471,49 @@ paths:
schema:
type: string
/w/{workspace}/workspaces/edit_large_file_storage_config:
post:
summary: edit large file storage settings
operationId: editLargeFileStorageConfig
tags:
- workspace
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: LargeFileStorage info
required: true
content:
application/json:
schema:
type: object
properties:
large_file_storage:
$ref: "#/components/schemas/LargeFileStorage"
responses:
"200":
description: status
content:
application/json:
schema: {}
/w/{workspace}/workspaces/get_large_file_storage_config:
get:
summary: get large file storage config
operationId: getLargeFileStorageConfig
tags:
- workspace
parameters:
- $ref: "#/components/parameters/WorkspaceId"
responses:
"200":
description: status
content:
application/json:
schema:
$ref: "#/components/schemas/LargeFileStorage"
/w/{workspace}/users/list:
get:
summary: list users
@@ -6231,6 +6276,69 @@ paths:
type: string
format: uuid
/w/{workspace}/job_helpers/duckdb_connection_settings:
post:
summary: Converts an S3 resource to the set of instructions necessary to connect DuckDB to an S3 bucket
operationId: duckdbConnectionSettings
tags:
- helpers
parameters:
- $ref: "#/components/parameters/WorkspaceId"
requestBody:
description: S3 resource to connect to
required: true
content:
application/json:
schema:
s3_resource:
$ref: "#/components/schemas/S3Resource"
responses:
"200":
description: Connection settings
content:
application/json:
schema:
type: object
properties:
connection_settings_str:
type: string
/w/{workspace}/job_helpers/test_connection:
get:
summary: Test connection to the workspace datasets storage
operationId: datasetStorageTestConnection
tags:
- helpers
parameters:
- $ref: "#/components/parameters/WorkspaceId"
responses:
"200":
description: Connection settings
content:
application/json:
schema: {}
/w/{workspace}/job_helpers/list_stored_datasets:
post:
summary: List the dataset keys available in the worspace datasets storage
operationId: polarsConnectionSettings
tags:
- helpers
parameters:
- $ref: "#/components/parameters/WorkspaceId"
responses:
"200":
description: Connection settings
content:
application/json:
schema:
type: object
properties:
dataset_keys:
type: array
items:
$ref: "#/components/schemas/WindmillLargeFile"
components:
securitySchemes:
bearerAuth:
@@ -8206,3 +8314,46 @@ components:
schema:
type: string
enum: [script, failure, trigger, approval]
LargeFileStorage:
type: object
properties:
type:
type: string
enum: ["S3Storage"]
s3_resource_path:
type: string
WindmillLargeFile:
type: object
properties:
s3:
type: string
s3_bucket:
type: string
required:
- s3
S3Resource:
type: object
properties:
bucket:
type: string
region:
type: string
endPoint:
type: string
useSSL:
type: boolean
accessKey:
type: string
secretKey:
type: string
pathStyle:
type: boolean
required:
- bucket
- region
- endPoint
- useSSL
- pathStyle

View File

@@ -0,0 +1,306 @@
use crate::{resources::transform_json_value, users::Tokened, workspaces::LargeFileStorage};
use aws_sdk_s3::config::{Credentials, Region};
use axum::{
extract::Path,
routing::{get, post},
Extension, Json, Router,
};
use hyper::http;
use serde::{Deserialize, Serialize};
use tower_http::cors::{Any, CorsLayer};
use windmill_common::{db::UserDB, error};
use crate::db::ApiAuthed;
pub fn workspaced_service() -> Router {
let cors = CorsLayer::new()
.allow_methods([http::Method::GET, http::Method::POST])
.allow_headers([http::header::CONTENT_TYPE, http::header::AUTHORIZATION])
.allow_origin(Any);
Router::new()
.route(
"/duckdb_connection_settings",
post(duckdb_connection_settings).layer(cors.clone()),
)
.route(
"/polars_connection_settings",
post(polars_connection_settings).layer(cors.clone()),
)
.route("/test_connection", get(test_connection).layer(cors.clone()))
.route(
"/list_stored_datasets",
get(list_stored_datasets).layer(cors.clone()),
)
}
#[derive(Debug, Deserialize, Clone)]
struct S3Resource {
#[serde(rename = "bucket")]
bucket: String,
region: String,
#[serde(rename = "endPoint")]
endpoint: String,
#[serde(rename = "useSSL")]
use_ssl: bool,
#[serde(rename = "accessKey")]
access_key: Option<String>,
#[serde(rename = "secretKey")]
secret_key: Option<String>,
#[serde(rename = "pathStyle")]
path_style: bool,
}
#[derive(Deserialize)]
struct DuckdbConnectionSettingsQuery {
s3_resource: S3Resource,
}
#[derive(Serialize)]
struct DuckdbConnectionSettingsResponse {
connection_settings_str: String,
}
async fn duckdb_connection_settings(
Path(_w_id): Path<String>,
Json(query): Json<DuckdbConnectionSettingsQuery>,
) -> error::JsonResult<DuckdbConnectionSettingsResponse> {
let mut duckdb_settings: String = String::new();
let s3_resource = query.s3_resource;
duckdb_settings.push_str(format!("SET home_directory='./';\n").as_str()); // TODO: make this configurable maybe, or point to a temporary folder
duckdb_settings.push_str(format!("INSTALL 'httpfs';\n").as_str());
if s3_resource.path_style {
duckdb_settings.push_str(format!("SET s3_url_style='path';\n").as_str());
}
duckdb_settings.push_str(format!("SET s3_region='{}';\n", s3_resource.region).as_str());
duckdb_settings.push_str(format!("SET s3_endpoint='{}';\n", s3_resource.endpoint).as_str());
if !s3_resource.use_ssl {
duckdb_settings.push_str(format!("SET s3_use_ssl=0;\n").as_str()); // default is true for DuckDB
}
if let Some(access_key_id) = s3_resource.access_key {
duckdb_settings.push_str(format!("SET s3_access_key_id='{}';\n", access_key_id).as_str());
}
if let Some(secret_access_key) = s3_resource.secret_key {
duckdb_settings
.push_str(format!("SET s3_secret_access_key='{}';\n", secret_access_key).as_str());
}
let response = DuckdbConnectionSettingsResponse { connection_settings_str: duckdb_settings };
return Ok(Json(response));
}
#[derive(Deserialize)]
struct PolarsConnectionSettingsQuery {
s3_resource: S3Resource,
}
#[derive(Serialize)]
struct PolarsConnectionSettingsResponse {
endpoint_url: String,
#[serde(skip_serializing_if = "Option::is_none")]
key: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
secret: Option<String>,
use_ssl: bool,
cache_regions: bool,
client_kwargs: PolarsConnectionSettings,
}
#[derive(Serialize)]
struct PolarsConnectionSettings {
pub region_name: String,
}
async fn polars_connection_settings(
Path(_w_id): Path<String>,
Json(query): Json<PolarsConnectionSettingsQuery>,
) -> error::JsonResult<PolarsConnectionSettingsResponse> {
let s3_resource = query.s3_resource;
let response = PolarsConnectionSettingsResponse {
endpoint_url: s3_resource.endpoint,
key: s3_resource.access_key,
secret: s3_resource.secret_key,
use_ssl: s3_resource.use_ssl,
cache_regions: false,
client_kwargs: PolarsConnectionSettings { region_name: s3_resource.region },
};
return Ok(Json(response));
}
#[derive(Serialize)]
struct ListStoredDatasetsResponse {
windmill_large_files: Vec<WindmillLargeFile>,
}
#[derive(Serialize, Clone)]
struct WindmillLargeFile {
s3: String,
s3_bucket: Option<String>,
}
async fn test_connection(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Tokened { token }: Tokened,
Path(w_id): Path<String>,
) -> error::JsonResult<()> {
let s3_resource_opt = get_workspace_s3_resource(&authed, &user_db, &token, &w_id).await?;
if s3_resource_opt.is_none() {
return Err(error::Error::NotFound(
"No datasets storage resource defined at the workspace level".to_string(),
));
}
let s3_resource = s3_resource_opt.unwrap();
let s3_client = build_s3_client(&s3_resource);
s3_client
.list_objects()
.bucket(s3_resource.bucket)
.max_keys(1)
.send()
.await
.map_err(|err| error::Error::InternalErr(err.to_string()))?;
return Ok(Json(()));
}
async fn list_stored_datasets(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
Tokened { token }: Tokened,
Path(w_id): Path<String>,
) -> error::JsonResult<ListStoredDatasetsResponse> {
let s3_resource_opt = get_workspace_s3_resource(&authed, &user_db, &token, &w_id).await?;
let s3_resource = s3_resource_opt.unwrap();
let s3_client = build_s3_client(&s3_resource);
let mut stored_datasets = Vec::<WindmillLargeFile>::new();
let s3_bucket = s3_resource.bucket;
loop {
let mut list_object_query = s3_client
.list_objects()
.bucket(s3_bucket.clone())
.max_keys(32);
if let Some(last_dataset_name) = stored_datasets.last() {
// if stored_datasets already has some elements, it means we're looping through pages
// according to AWS SDK docs, we can set the marker to the last returned dataset
list_object_query = list_object_query.set_marker(Some(last_dataset_name.clone().s3))
}
let bucket_objects = list_object_query
.send()
.await
.map_err(|err| error::Error::InternalErr(err.to_string()))?;
let page_datasets = bucket_objects
.contents()
.iter()
.filter(|object| object.key().is_some())
.map(|object| object.key())
.map(Option::unwrap)
.map(&str::to_string)
.map(|object_key| WindmillLargeFile {
s3: object_key.clone(),
s3_bucket: Some(s3_bucket.clone()),
})
.collect::<Vec<WindmillLargeFile>>();
stored_datasets.extend(page_datasets.clone());
if !bucket_objects.is_truncated() {
break;
}
}
return Ok(Json(ListStoredDatasetsResponse {
windmill_large_files: stored_datasets,
}));
}
async fn get_workspace_s3_resource<'c>(
authed: &ApiAuthed,
user_db: &UserDB,
token: &str,
w_id: &str,
) -> error::Result<Option<S3Resource>> {
let mut tx = user_db.clone().begin(authed).await?;
let raw_lfs_opt = sqlx::query_scalar!(
"SELECT large_file_storage FROM workspace_settings WHERE workspace_id = $1",
w_id
)
.fetch_optional(&mut *tx)
.await?
.flatten();
tx.commit().await?;
if raw_lfs_opt.is_none() {
return Ok(None);
}
let large_file_storage = serde_json::from_value::<LargeFileStorage>(
raw_lfs_opt.unwrap(),
)
.map_err(|err| {
tracing::error!(
"Value stored in large_file_storage column is invalid and could not be deserialized: {}",
err
);
error::Error::InternalErr(
"Could not deserialize LargeFileStorage value found in database".to_string(),
)
})?;
let s3_lfs = match large_file_storage {
LargeFileStorage::S3Storage(s3_lfs) => s3_lfs,
};
let resource_path_json_value = serde_json::to_value(s3_lfs.s3_resource_path)
.map_err(|err| error::Error::InternalErr(err.to_string()))?;
let interpolated_value = transform_json_value(
authed,
user_db,
&w_id,
resource_path_json_value,
&Option::None,
token,
)
.await?;
let s3_resource = serde_json::from_value::<S3Resource>(interpolated_value)
.map_err(|err| error::Error::InternalErr(err.to_string()))?;
return Ok(Some(s3_resource));
}
fn build_s3_client(s3_resource_ref: &S3Resource) -> aws_sdk_s3::Client {
let s3_resource = s3_resource_ref.clone();
let endpoint_with_prefix = if s3_resource.endpoint.starts_with("http://")
|| s3_resource.endpoint.starts_with("https://")
{
s3_resource.endpoint.clone()
} else if s3_resource.use_ssl {
format!("https://{}", s3_resource.endpoint)
} else {
format!("http://{}", s3_resource.endpoint)
};
let mut s3_config_builder = aws_sdk_s3::Config::builder()
.endpoint_url(endpoint_with_prefix)
.region(Region::new(s3_resource.region));
if s3_resource.access_key.is_some() {
s3_config_builder = s3_config_builder.credentials_provider(Credentials::new(
s3_resource.access_key.unwrap_or_default(),
s3_resource.secret_key.unwrap_or_default(),
None,
None,
"s3_storage",
));
}
if s3_resource.path_style {
s3_config_builder = s3_config_builder.force_path_style(true);
}
let s3_config = s3_config_builder.build();
return aws_sdk_s3::Client::from_conf(s3_config);
}

View File

@@ -57,6 +57,7 @@ mod granular_acls;
mod groups;
mod inputs;
mod integration;
pub mod job_helpers;
pub mod jobs;
pub mod oauth2;
mod openai;
@@ -179,31 +180,32 @@ pub async fn run_server(
// Reordered alphabetically
.nest("/acls", granular_acls::workspaced_service())
.nest("/apps", apps::workspaced_service())
.nest("/raw_apps", raw_apps::workspaced_service())
.nest("/audit", audit::workspaced_service())
.nest("/capture", capture::workspaced_service())
.nest(
"/embeddings",
embeddings::workspaced_service(embeddings_db.clone()),
)
.nest("/drafts", drafts::workspaced_service())
.nest("/favorites", favorite::workspaced_service())
.nest("/flows", flows::workspaced_service())
.nest("/folders", folders::workspaced_service())
.nest("/groups", groups::workspaced_service())
.nest("/inputs", inputs::workspaced_service())
.nest("/job_helpers", job_helpers::workspaced_service())
.nest("/jobs", jobs::workspaced_service())
.nest("/oauth", oauth2::workspaced_service())
.nest("/openai", openai::workspaced_service())
.nest("/raw_apps", raw_apps::workspaced_service())
.nest("/resources", resources::workspaced_service())
.nest("/schedules", schedule::workspaced_service())
.nest("/scripts", scripts::workspaced_service())
.nest("/drafts", drafts::workspaced_service())
.nest(
"/users",
users::workspaced_service().layer(Extension(argon2.clone())),
)
.nest("/variables", variables::workspaced_service())
.nest("/workspaces", workspaces::workspaced_service())
.nest("/openai", openai::workspaced_service())
.nest(
"/embeddings",
embeddings::workspaced_service(embeddings_db.clone()),
),
.nest("/workspaces", workspaces::workspaced_service()),
)
.nest("/workspaces", workspaces::global_service())
.nest(

View File

@@ -79,7 +79,8 @@ pub fn workspaced_service() -> Router {
.route("/premium_info", get(premium_info))
.route("/edit_copilot_config", post(edit_copilot_config))
.route("/get_copilot_info", get(get_copilot_info) )
.route("/edit_error_handler", post(edit_error_handler));
.route("/edit_error_handler", post(edit_error_handler))
.route("/edit_large_file_storage_config", post(edit_large_file_storage_config));
#[cfg(feature = "enterprise")]
{
@@ -143,6 +144,7 @@ pub struct WorkspaceSettings {
pub error_handler: Option<String>,
pub error_handler_extra_args: Option<serde_json::Value>,
pub error_handler_muted_on_cancel: Option<bool>,
pub large_file_storage: Option<serde_json::Value>, // effectively: DatasetsStorage
}
#[derive(FromRow, Serialize, Debug)]
@@ -199,6 +201,11 @@ struct EditCopilotConfig {
code_completion_enabled: bool,
}
#[derive(Deserialize)]
struct EditLargeFileStorageConfig {
large_file_storage: Option<LargeFileStorage>,
}
#[derive(Deserialize)]
struct CreateWorkspace {
id: String,
@@ -258,6 +265,18 @@ pub struct EditErrorHandler {
pub error_handler_muted_on_cancel: Option<bool>,
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(tag = "type")]
pub enum LargeFileStorage {
S3Storage(S3Storage),
// TODO: Add a filesystem type here in the future if needed
}
#[derive(Serialize, Deserialize, Debug)]
pub struct S3Storage {
pub s3_resource_path: String,
}
async fn list_pending_invites(
authed: ApiAuthed,
Extension(user_db): Extension<UserDB>,
@@ -885,6 +904,52 @@ async fn get_copilot_info(
}))
}
async fn edit_large_file_storage_config(
authed: ApiAuthed,
Extension(db): Extension<DB>,
Path(w_id): Path<String>,
ApiAuthed { is_admin, username, .. }: ApiAuthed,
Json(new_config): Json<EditLargeFileStorageConfig>,
) -> Result<String> {
require_admin(is_admin, &username)?;
let mut tx = db.begin().await?;
let args_for_audit = format!("{:?}", new_config.large_file_storage);
audit_log(
&mut *tx,
&authed.username,
"workspaces.edit_large_file_storage_config",
ActionKind::Update,
&w_id,
Some(&authed.email),
Some([("large_file_storage", args_for_audit.as_str())].into()),
)
.await?;
if let Some(lfs_config) = new_config.large_file_storage {
let serialized_lfs_config = serde_json::to_value::<LargeFileStorage>(lfs_config)
.map_err(|err| Error::InternalErr(err.to_string()))?;
sqlx::query!(
"UPDATE workspace_settings SET large_file_storage = $1 WHERE workspace_id = $2",
serialized_lfs_config,
&w_id
)
.execute(&mut *tx)
.await?;
} else {
sqlx::query!(
"UPDATE workspace_settings SET large_file_storage = NULL WHERE workspace_id = $1",
&w_id,
)
.execute(&mut *tx)
.await?;
}
tx.commit().await?;
Ok(format!("Edit copilot config for workspace {}", &w_id))
}
async fn edit_error_handler(
authed: ApiAuthed,

View File

@@ -14,7 +14,13 @@
import Tooltip from '$lib/components/Tooltip.svelte'
import WorkspaceUserSettings from '$lib/components/settings/WorkspaceUserSettings.svelte'
import { WORKSPACE_SHOW_SLACK_CMD, WORKSPACE_SHOW_WEBHOOK_CLI_SYNC } from '$lib/consts'
import { OauthService, Script, WorkspaceService } from '$lib/gen'
import {
LargeFileStorage,
OauthService,
Script,
WorkspaceService,
HelpersService
} from '$lib/gen'
import {
enterpriseLicense,
copilotInfo,
@@ -47,6 +53,7 @@
let errorHandlerExtraArgs: Record<string, any> = {}
let errorHandlerMutedOnCancel: boolean | undefined = undefined
let openaiResourceInitialPath: string | undefined = undefined
let s3ResourceInitialPath: string | undefined = undefined
let codeCompletionEnabled: boolean = false
let tab =
($page.url.searchParams.get('tab') as
@@ -148,6 +155,31 @@
sendUserToast(`Copilot settings updated`)
}
async function editWindmillLFSSettings(s3ResourcePath: string): Promise<void> {
s3ResourceInitialPath = s3ResourcePath
if (s3ResourcePath) {
let resourcePathWithPrefix = `$res:${s3ResourcePath}`
await WorkspaceService.editLargeFileStorageConfig({
workspace: $workspaceStore!,
requestBody: {
large_file_storage: {
type: LargeFileStorage.type.S3STORAGE,
s3_resource_path: resourcePathWithPrefix
}
}
})
sendUserToast(`Large file storage settings updated`)
} else {
await WorkspaceService.editLargeFileStorageConfig({
workspace: $workspaceStore!,
requestBody: {
large_file_storage: undefined
}
})
sendUserToast(`Large file storage settings reset`)
}
}
async function loadSettings(): Promise<void> {
const settings = await WorkspaceService.getSettings({ workspace: $workspaceStore! })
team_name = settings.slack_name
@@ -177,6 +209,11 @@
}
errorHandlerExtraArgs = settings.error_handler_extra_args ?? {}
codeCompletionEnabled = settings.code_completion_enabled
console.log(settings)
s3ResourceInitialPath =
settings.large_file_storage?.type === LargeFileStorage.type.S3STORAGE
? settings.large_file_storage?.s3_resource_path?.replace('$res:', '')
: undefined
}
$: {
@@ -259,7 +296,6 @@
<Tab size="xs" value="error_handler">
<div class="flex gap-2 items-center my-1">Error Handler</div>
</Tab>
<Tab size="xs" value="openai">
<div class="flex gap-2 items-center my-1">Windmill AI</div>
</Tab>
@@ -543,6 +579,33 @@
}}
/>
</div>
{:else if tab == 'windmill_lfs'}
<PageHeader title="Windmill Large File Storage" primary={false} />
<div class="mt-5 flex gap-1">
{#key s3ResourceInitialPath}
<ResourcePicker
resourceType="s3"
initialValue={s3ResourceInitialPath}
on:change={(ev) => {
editWindmillLFSSettings(ev.detail)
}}
/>
{/key}
<Button
size="sm"
variant="contained"
color="dark"
disabled={!s3ResourceInitialPath}
on:click={async () => {
if ($workspaceStore) {
await HelpersService.datasetStorageTestConnection({
workspace: $workspaceStore
})
sendUserToast('Connection successful')
}
}}>Test Connection</Button
>
</div>
{/if}
{:else}
<div class="bg-red-100 border-l-4 border-red-600 text-orange-700 p-4 m-4" role="alert">

View File

@@ -1 +0,0 @@
windmill-api/

View File

@@ -40,4 +40,4 @@ mv windmill-api/README.md.tmp windmill-api/README.md
cd windmill-api && poetry build
cd ../wmill && poetry build
cd ../wmill_pg && poetry build
cd .. && echo "windmill-api/" >> .gitignore
cd .. && echo "windmill-api/" >> .gitignore

View File

@@ -0,0 +1,16 @@
Testing pythong wmill client
============================
Make sure you have a local windmill BE running and listening on localhost:8000 (either using cargo run or via the docker compose).
Install the local package to your virtual env:
```bash
# this is necessary only if you made a change to the BE API via the openapi.yml file
cd ./windmill-api # you can generate it using the build.sh script at the root of this repo
pip3 install .
cd ./wmill
pip3 install .
```
Then you can run go to `wmill_client_test.py` and add a Token and Workspace if necessary. You can then implement your own test calling any function in the wmill client and test its output.

View File

@@ -0,0 +1,69 @@
import unittest
import wmill
import os
class TestStringMethods(unittest.TestCase):
_token = "<TOKEN>"
_workspace = "<WORKSPACE>"
def setUp(self):
os.environ["WM_WORKSPACE"] = self._workspace
os.environ["WM_TOKEN"] = self._token
def test_duckdb_connection_settings(self):
s3_resource = {
"port": 9000,
"bucket": "windmill",
"region": "fr-paris",
"useSSL": False,
"endPoint": "localhost:9000",
"accessKey": "ACCESS_KEY",
"pathStyle": True,
"secretKey": "SECRET_KEY",
}
settings = wmill.duckdb_connection_settings(s3_resource)
self.assertIsNotNone(settings)
expected_settings_str = """SET home_directory='./shared/';
INSTALL 'httpfs';
SET s3_url_style='path';
SET s3_region='fr-paris';
SET s3_endpoint='localhost:9000';
SET s3_use_ssl=0;
SET s3_access_key_id='ACCESS_KEY';
SET s3_secret_access_key='SECRET_KEY';
"""
self.assertEqual(settings, {"connection_settings_str": expected_settings_str})
settings = wmill.polars_connection_settings(s3_resource)
print(settings)
def test_polars_connection_settings(self):
s3_resource = {
"port": 9000,
"bucket": "windmill",
"region": "fr-paris",
"useSSL": False,
"endPoint": "localhost:9000",
"accessKey": "ACCESS_KEY",
"pathStyle": True,
"secretKey": "SECRET_KEY",
}
settings = wmill.polars_connection_settings(s3_resource)
print(settings)
expected_settings = {
"cache_regions": False,
"client_kwargs": {"region_name": "fr-paris"},
"endpoint_url": "localhost:9000",
"key": "ACCESS_KEY",
"secret": "SECRET_KEY",
"use_ssl": False,
}
self.assertEqual(settings, expected_settings)
if __name__ == "__main__":
unittest.main()

View File

@@ -34,9 +34,7 @@ class JobStatus(Enum):
_client: "AuthenticatedClient | None" = None
def create_client(
base_url: "str | None" = None, token: "str | None" = None
) -> AuthenticatedClient:
def create_client(base_url: "str | None" = None, token: "str | None" = None) -> AuthenticatedClient:
env_base_url = os.environ.get("BASE_INTERNAL_URL")
if env_base_url is not None:
@@ -46,9 +44,7 @@ def create_client(
token_: str = token or os.environ.get("WM_TOKEN") or ""
global _client
if _client is None:
_client = AuthenticatedClient(
base_url=base_url_, token=token_, timeout=30, verify_ssl=False
)
_client = AuthenticatedClient(base_url=base_url_, token=token_, timeout=30, verify_ssl=False)
return _client
@@ -65,9 +61,7 @@ def get_version() -> str:
"""
from windmill_api.api.settings import backend_version
return backend_version.sync_detailed(client=create_client()).content.decode(
"us-ascii"
)
return backend_version.sync_detailed(client=create_client()).content.decode("us-ascii")
def run_script_async(
@@ -92,9 +86,7 @@ def run_script_async(
).content.decode("us-ascii")
def run_script_sync(
hash: str, args: Dict[str, Any] = {}, verbose: bool = False
) -> Dict[str, Any]:
def run_script_sync(hash: str, args: Dict[str, Any] = {}, verbose: bool = False) -> Dict[str, Any]:
"""
Run a script, wait for it to complete and return the result of the launched script
"""
@@ -133,9 +125,7 @@ def run_script_by_path_async(
).content.decode("us-ascii")
def run_script_by_path_sync(
path: str, args: Dict[str, Any] = {}, verbose: bool = False
) -> Dict[str, Any]:
def run_script_by_path_sync(path: str, args: Dict[str, Any] = {}, verbose: bool = False) -> Dict[str, Any]:
"""
Run a script, wait for it to complete and return the result of the launched script
"""
@@ -159,9 +149,7 @@ def get_job_status(job_id: str) -> JobStatus:
from windmill_api.models.get_job_response_200_type import GetJobResponse200Type
from windmill_api.api.job import get_job
res = get_job.sync_detailed(
client=create_client(), workspace=get_workspace(), id=job_id
).parsed
res = get_job.sync_detailed(client=create_client(), workspace=get_workspace(), id=job_id).parsed
if not res:
raise Exception(f"Job {job_id} not found")
elif not res.type:
@@ -183,9 +171,7 @@ def get_result(job_id: str) -> Dict[str, Any]:
"""
from windmill_api.api.job import get_completed_job
res = get_completed_job.sync_detailed(
client=create_client(), workspace=get_workspace(), id=job_id
).parsed
res = get_completed_job.sync_detailed(client=create_client(), workspace=get_workspace(), id=job_id).parsed
if not res:
raise Exception(f"Job {job_id} not found")
if not res.result:
@@ -203,9 +189,7 @@ def get_resource(path: str | None = None, none_if_undefined: bool = False) -> An
)
path = path or get_state_path()
parsed = get_resource_api.sync_detailed(
workspace=get_workspace(), path=path, client=create_client()
)
parsed = get_resource_api.sync_detailed(workspace=get_workspace(), path=path, client=create_client())
try:
content = parsed.content.decode("utf-8")
parsed = json.loads(content)
@@ -223,6 +207,63 @@ def get_resource(path: str | None = None, none_if_undefined: bool = False) -> An
return parsed
def duckdb_connection_settings(s3_resource: Any, none_if_undefined: bool = False) -> str | None:
"""
Convenient helpers that takes an S3 resource as input and returns the settings necessary to
initiate an S3 connection from DuckDB
"""
from windmill_api.api.helpers import duckdb_connection_settings
from windmill_api.models.s3_resource import S3Resource
parsed = duckdb_connection_settings.sync_detailed(
workspace=get_workspace(),
client=create_client(),
json_body={
"s3_resource": s3_resource,
},
)
try:
content = parsed.content.decode("utf-8")
parsed = json.loads(content)
except:
parsed = None
if parsed is None:
if none_if_undefined:
return None
else:
raise Exception(f"Could not generate DuckDB S3 connection settings from the provided resource")
return parsed
def polars_connection_settings(s3_resource: Any, none_if_undefined: bool = False) -> Any:
"""
Convenient helpers that takes an S3 resource as input and returns the settings necessary to
initiate an S3 connection from Polars
"""
from windmill_api.api.helpers import polars_connection_settings
parsed = polars_connection_settings.sync_detailed(
workspace=get_workspace(),
client=create_client(),
json_body={
"s3_resource": s3_resource,
},
)
try:
content = parsed.content.decode("utf-8")
parsed = json.loads(content)
except:
parsed = None
if parsed is None:
if none_if_undefined:
return None
else:
raise Exception(f"Could not generate Polars S3 connection settings from the provided resource")
return parsed
def whoami() -> WhoamiResponse200 | None:
"""
Returns the current user
@@ -239,9 +280,7 @@ def get_state() -> Any:
return get_resource(None, True)
def set_resource(
value: Any, path: str | None = None, resource_type: str = "state"
) -> None:
def set_resource(value: Any, path: str | None = None, resource_type: str = "state") -> None:
"""
Set the resource at a given path as a string, creating it if it does not exist
"""
@@ -258,15 +297,11 @@ def set_resource(
path = path or get_state_path()
workspace = get_workspace()
client = create_client()
if not exists_resource.sync_detailed(
workspace=workspace, path=path, client=client
).parsed:
if not exists_resource.sync_detailed(workspace=workspace, path=path, client=client).parsed:
create_resource.sync_detailed(
workspace=workspace,
client=client,
json_body=CreateResourceJsonBody(
path=path, value=value, resource_type=resource_type
),
json_body=CreateResourceJsonBody(path=path, value=value, resource_type=resource_type),
)
else:
update_resource_value.sync_detailed(
@@ -330,9 +365,7 @@ def get_variable(path: str) -> str:
"""
from windmill_api.api.variable import get_variable_value as get_variable_api
res = get_variable_api.sync_detailed(
workspace=get_workspace(), path=path, client=create_client()
)
res = get_variable_api.sync_detailed(workspace=get_workspace(), path=path, client=create_client())
parsed = res.parsed
if parsed is None:
raise Exception(
@@ -357,15 +390,11 @@ def set_variable(path: str, value: str) -> None:
workspace = get_workspace()
client = create_client()
if not exists_variable.sync_detailed(
workspace=workspace, path=path, client=client
).parsed:
if not exists_variable.sync_detailed(workspace=workspace, path=path, client=client).parsed:
create_variable.sync_detailed(
workspace=workspace,
client=client,
json_body=CreateVariableJsonBody(
path=path, value=value, is_secret=False, description=""
),
json_body=CreateVariableJsonBody(path=path, value=value, is_secret=False, description=""),
)
else:
update_variable.sync_detailed(
@@ -392,9 +421,7 @@ def get_resume_urls(approver: str | None = None) -> Dict:
import random
nonce = random.randint(0, 1000000000)
res = get_resume_urls_api.sync_detailed(
workspace, job_id, nonce, client=client, approver=approver
)
res = get_resume_urls_api.sync_detailed(workspace, job_id, nonce, client=client, approver=approver)
if res.parsed is not None:
return res.parsed.to_dict()
else: