arraylake 1.5.0__tar.gz → 1.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {arraylake-1.5.0 → arraylake-1.7.0}/PKG-INFO +4 -4
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/_version.py +2 -2
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/cli/compute.py +20 -5
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/cli/config.py +3 -3
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/cli/iceberg.py +16 -4
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/cli/repo.py +12 -4
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/client.py +20 -14
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/compute/http_client.py +12 -1
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/compute/services.py +23 -4
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/compute/types.py +12 -2
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/metastore/http_metastore.py +1 -1
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/repos/icechunk/virtual.py +6 -6
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/tuning.py +2 -2
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/types.py +87 -17
- {arraylake-1.5.0 → arraylake-1.7.0}/pyproject.toml +10 -25
- {arraylake-1.5.0 → arraylake-1.7.0}/tests/compute/test_client.py +43 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/tests/icechunk/test_virtual.py +20 -4
- {arraylake-1.5.0 → arraylake-1.7.0}/tests/test_cli.py +6 -1
- {arraylake-1.5.0 → arraylake-1.7.0}/tests/test_client.py +1 -2
- {arraylake-1.5.0 → arraylake-1.7.0}/tests/test_iceberg_client.py +6 -2
- {arraylake-1.5.0 → arraylake-1.7.0}/tests/test_subscriptions.py +11 -11
- {arraylake-1.5.0 → arraylake-1.7.0}/tests/test_types.py +28 -2
- {arraylake-1.5.0 → arraylake-1.7.0}/tests/test_virtual_chunks.py +3 -3
- {arraylake-1.5.0 → arraylake-1.7.0}/uv.lock +390 -310
- {arraylake-1.5.0 → arraylake-1.7.0}/.gitignore +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/README.md +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/__init__.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/__main__.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/_credential_cache.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/api_utils.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/asyn.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/cli/__init__.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/cli/auth.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/cli/main.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/cli/org.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/cli/skills.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/cli/utils.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/config.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/config.yaml +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/credentials.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/diagnostics.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/display/__init__.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/display/repo.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/display/repolist.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/exceptions.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/log_util.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/metastore/__init__.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/metastore/abc.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/obstore.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/py.typed +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/repos/__init__.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/repos/iceberg/__init__.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/repos/iceberg/auth.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/repos/iceberg/catalog.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/repos/icechunk/__init__.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/repos/icechunk/storage.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/repos/icechunk/types.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/arraylake/token.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/tests/__init__.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/tests/compute/test_cli_table.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/tests/config.yaml +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/tests/conftest.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/tests/icechunk/__init__.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/tests/icechunk/conftest.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/tests/icechunk/test_storage.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/tests/test_api_utils.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/tests/test_asyn.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/tests/test_config.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/tests/test_credential_cache.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/tests/test_credentials.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/tests/test_diagnostics.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/tests/test_http_metastore.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/tests/test_obstore.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/tests/test_performance.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/tests/test_skills.py +0 -0
- {arraylake-1.5.0 → arraylake-1.7.0}/tests/test_token_handler.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: arraylake
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.7.0
|
|
4
4
|
Summary: Python client for ArrayLake
|
|
5
5
|
Project-URL: Documentation, https://docs.earthmover.io/
|
|
6
6
|
Project-URL: Changelog, https://docs.earthmover.io/changelog
|
|
@@ -15,23 +15,23 @@ Requires-Dist: click<9.0,>=8.1.3
|
|
|
15
15
|
Requires-Dist: donfig<1.0,>=0.7
|
|
16
16
|
Requires-Dist: httpx<0.29,>=0.23
|
|
17
17
|
Requires-Dist: icechunk<3.0.0,>=2.2.2
|
|
18
|
-
Requires-Dist: numpy<3.0,>=1.23
|
|
19
18
|
Requires-Dist: packaging<27.0,>=23.0
|
|
20
19
|
Requires-Dist: pydantic[email]<3.0,>=2.9
|
|
21
20
|
Requires-Dist: python-dateutil<3.0,>=2.8
|
|
21
|
+
Requires-Dist: pyyaml<7.0,>=6.0.1
|
|
22
22
|
Requires-Dist: rich<16.0,>=12.6
|
|
23
23
|
Requires-Dist: ruamel-yaml<1.0,>=0.17
|
|
24
24
|
Requires-Dist: structlog<27.0,>=24.1
|
|
25
25
|
Requires-Dist: typer<1.0,>=0.12.0
|
|
26
26
|
Requires-Dist: zarr<4.0,>=3.1.0
|
|
27
27
|
Provides-Extra: iceberg
|
|
28
|
-
Requires-Dist: pyiceberg<0.13,>=0.11; extra == 'iceberg'
|
|
28
|
+
Requires-Dist: pyiceberg[pyarrow]<0.13,>=0.11; extra == 'iceberg'
|
|
29
29
|
Provides-Extra: maximal
|
|
30
30
|
Requires-Dist: xarray[accel,etc,io,parallel]>=2024.10.0; extra == 'maximal'
|
|
31
31
|
Provides-Extra: obstore
|
|
32
32
|
Requires-Dist: obstore>=0.9.2; extra == 'obstore'
|
|
33
33
|
Provides-Extra: openeo
|
|
34
|
-
Requires-Dist: openeo<0.
|
|
34
|
+
Requires-Dist: openeo<0.53,>=0.35.0; extra == 'openeo'
|
|
35
35
|
Provides-Extra: xarray
|
|
36
36
|
Requires-Dist: xarray>=2024.10.0; extra == 'xarray'
|
|
37
37
|
Description-Content-Type: text/markdown
|
|
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
|
|
|
18
18
|
commit_id: str | None
|
|
19
19
|
__commit_id__: str | None
|
|
20
20
|
|
|
21
|
-
__version__ = version = '1.
|
|
22
|
-
__version_tuple__ = version_tuple = (1,
|
|
21
|
+
__version__ = version = '1.7.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 7, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -3,6 +3,7 @@ from enum import StrEnum
|
|
|
3
3
|
|
|
4
4
|
import typer
|
|
5
5
|
from rich import print_json
|
|
6
|
+
from rich.markup import escape
|
|
6
7
|
from rich.table import Table
|
|
7
8
|
|
|
8
9
|
from arraylake import Client
|
|
@@ -25,6 +26,7 @@ def _services_table(services, org):
|
|
|
25
26
|
table.add_column("Service Type", justify="right", style="green", min_width=10)
|
|
26
27
|
table.add_column("Org", justify="right", style="green", min_width=10)
|
|
27
28
|
table.add_column("Public", justify="right", style="green", min_width=10)
|
|
29
|
+
table.add_column("Region", justify="right", style="green", min_width=10)
|
|
28
30
|
table.add_column("URL", justify="right", style="green", min_width=10)
|
|
29
31
|
table.add_column("Status", justify="right", style="green", min_width=10)
|
|
30
32
|
|
|
@@ -45,6 +47,7 @@ def _services_table(services, org):
|
|
|
45
47
|
service.config.service_type,
|
|
46
48
|
service.config.org,
|
|
47
49
|
str(service.config.is_public),
|
|
50
|
+
service.region or "",
|
|
48
51
|
service.url,
|
|
49
52
|
service.status,
|
|
50
53
|
# .get, because the API owns this vocabulary and adds to it: an older client must
|
|
@@ -67,6 +70,12 @@ def enable_service(
|
|
|
67
70
|
"-n",
|
|
68
71
|
help="Custom deployment name (defaults to protocol name). Must be lowercase alphanumeric with dashes, max 16 chars.",
|
|
69
72
|
),
|
|
73
|
+
region: str | None = typer.Option(
|
|
74
|
+
None,
|
|
75
|
+
"--region",
|
|
76
|
+
"-r",
|
|
77
|
+
help="The region to run the service in, e.g. eu-west-1. Defaults to the environment's default region.",
|
|
78
|
+
),
|
|
70
79
|
):
|
|
71
80
|
"""**Enable** a new service
|
|
72
81
|
|
|
@@ -83,13 +92,19 @@ def enable_service(
|
|
|
83
92
|
```
|
|
84
93
|
$ arraylake compute enable my-org tiles --sku-code s --deployment-name my-tiles
|
|
85
94
|
```
|
|
95
|
+
|
|
96
|
+
- Enable an EDR service in Europe
|
|
97
|
+
|
|
98
|
+
```
|
|
99
|
+
$ arraylake compute enable my-org edr --sku-code xs --region eu-west-1
|
|
100
|
+
```
|
|
86
101
|
"""
|
|
87
|
-
kwargs = {}
|
|
102
|
+
kwargs: dict[str, str] = {}
|
|
88
103
|
if deployment_name is not None:
|
|
89
104
|
kwargs["deployment_name"] = deployment_name
|
|
90
105
|
|
|
91
106
|
with simple_progress(f"Creating [bold]{protocol}[/bold] service for [bold]{org}[/bold]..."):
|
|
92
|
-
status = Client().get_services(org).enable(protocol, is_public, sku_code=sku_code, **kwargs)
|
|
107
|
+
status = Client().get_services(org).enable(protocol, is_public, sku_code=sku_code, region=region, **kwargs)
|
|
93
108
|
rich_console.print(status["message"])
|
|
94
109
|
|
|
95
110
|
|
|
@@ -190,8 +205,8 @@ async def get_service_logs(
|
|
|
190
205
|
|
|
191
206
|
async for log in log_stream:
|
|
192
207
|
rich_console.print(log)
|
|
193
|
-
except RuntimeError:
|
|
194
|
-
rich_console.print(f"[bold red]
|
|
208
|
+
except RuntimeError as err:
|
|
209
|
+
rich_console.print(f"[bold red]{escape(str(err))}[/bold red]")
|
|
195
210
|
raise typer.Exit(code=1)
|
|
196
211
|
|
|
197
212
|
|
|
@@ -210,7 +225,7 @@ def update_service(
|
|
|
210
225
|
- Resize a service with ID edr-1234567 to a different compute shape
|
|
211
226
|
|
|
212
227
|
```
|
|
213
|
-
$ arraylake compute update my-org edr-1234567 --sku
|
|
228
|
+
$ arraylake compute update my-org edr-1234567 --sku m
|
|
214
229
|
```
|
|
215
230
|
|
|
216
231
|
- Change the scaling bounds of a service
|
|
@@ -162,7 +162,7 @@ def unset(
|
|
|
162
162
|
@app.command()
|
|
163
163
|
def get(
|
|
164
164
|
key: str = typer.Argument(..., help="key to get in config file"),
|
|
165
|
-
path: Path = typer.Option(
|
|
165
|
+
path: Path | None = typer.Option(None, help="Config file to read. Defaults to the active config."),
|
|
166
166
|
output: ListOutputType = typer.Option("yaml", help="Output formatting"),
|
|
167
167
|
):
|
|
168
168
|
"""**Get** Arraylake config value
|
|
@@ -191,11 +191,11 @@ def get(
|
|
|
191
191
|
def init(path: Path = typer.Option(user_config_file, help="Config file to initialize")):
|
|
192
192
|
"""**Initialize** Arraylake config
|
|
193
193
|
|
|
194
|
-
|
|
194
|
+
Creates the config file if it does not exist.
|
|
195
195
|
"""
|
|
196
196
|
|
|
197
197
|
c = get_user_config(path=path)
|
|
198
198
|
|
|
199
199
|
# write back to file
|
|
200
200
|
write_user_config(c, path=path)
|
|
201
|
-
rich_console.print(f"
|
|
201
|
+
rich_console.print(f"Config file: {path}")
|
|
@@ -109,7 +109,7 @@ def list_namespaces(
|
|
|
109
109
|
filter_metadata: str | None = typer.Option(None, help="JSON metadata filter"),
|
|
110
110
|
sort: NamespaceSort | None = None,
|
|
111
111
|
direction: Direction = Direction.desc,
|
|
112
|
-
include_ghosts: bool = False,
|
|
112
|
+
include_ghosts: bool = typer.Option(False, help="Also list deleted namespaces that you can still restore"),
|
|
113
113
|
page_size: int = typer.Option(50, min=1, max=100),
|
|
114
114
|
output: Output = Output.rich,
|
|
115
115
|
):
|
|
@@ -195,10 +195,22 @@ def delete_namespace(
|
|
|
195
195
|
):
|
|
196
196
|
"""Delete an empty namespace. A soft-delete by default: restore it during its recovery period."""
|
|
197
197
|
if not confirm:
|
|
198
|
-
|
|
199
|
-
|
|
198
|
+
typer.confirm(
|
|
199
|
+
f"{_namespace_delete_outcome(org, name, immediate=immediate, retain_data=retain_data, done=False)} Continue?", abort=True
|
|
200
|
+
)
|
|
200
201
|
Client().delete_iceberg_namespace(org, name, imsure=True, immediate=immediate, retain_data=retain_data)
|
|
201
|
-
typer.echo(
|
|
202
|
+
typer.echo(_namespace_delete_outcome(org, name, immediate=immediate, retain_data=retain_data, done=True))
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _namespace_delete_outcome(org: str, name: str, *, immediate: bool, retain_data: bool, done: bool) -> str:
|
|
206
|
+
verb = "Deleted namespace" if done else "This will delete namespace"
|
|
207
|
+
if not immediate:
|
|
208
|
+
return (
|
|
209
|
+
f"{verb} {org}/{name}. Until the grace period ends, you can restore it with `arraylake iceberg namespace restore {org} {name}`."
|
|
210
|
+
)
|
|
211
|
+
if retain_data:
|
|
212
|
+
return f"{verb} {org}/{name} from Arraylake. Its data stays in your storage bucket."
|
|
213
|
+
return f"{verb} {org}/{name} and its data. You cannot restore it."
|
|
202
214
|
|
|
203
215
|
|
|
204
216
|
@namespace_app.command("restore")
|
|
@@ -253,7 +253,7 @@ async def delete(
|
|
|
253
253
|
"""
|
|
254
254
|
if not confirm:
|
|
255
255
|
confirm = typer.confirm(
|
|
256
|
-
f"
|
|
256
|
+
f"{_delete_outcome(repo_name, immediate=immediate, retain_data=retain_data, done=False)} Continue?",
|
|
257
257
|
abort=True,
|
|
258
258
|
)
|
|
259
259
|
|
|
@@ -262,8 +262,16 @@ async def delete(
|
|
|
262
262
|
with simple_progress(f"Deleting repo [bold]{repo_name}[/bold]..."):
|
|
263
263
|
await client.delete_repo(repo_name, imsure=confirm, imreallysure=confirm, immediate=immediate, retain_data=retain_data)
|
|
264
264
|
|
|
265
|
-
|
|
266
|
-
|
|
265
|
+
rich_console.print(_delete_outcome(repo_name, immediate=immediate, retain_data=retain_data, done=True))
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _delete_outcome(repo_name: str, *, immediate: bool, retain_data: bool, done: bool) -> str:
|
|
269
|
+
verb = "Deleted" if done else "This will delete"
|
|
270
|
+
if not immediate:
|
|
271
|
+
return f"{verb} {repo_name}. You can restore it from the web app until the grace period ends."
|
|
272
|
+
if retain_data:
|
|
273
|
+
return f"{verb} {repo_name} from Arraylake. Its data stays in your storage bucket."
|
|
274
|
+
return f"{verb} {repo_name} and its data. You cannot restore it."
|
|
267
275
|
|
|
268
276
|
|
|
269
277
|
@app.command()
|
|
@@ -339,7 +347,7 @@ async def set_status(
|
|
|
339
347
|
@coro # type: ignore
|
|
340
348
|
async def tune(
|
|
341
349
|
org_name: str = typer.Argument(..., help="Name of organization"),
|
|
342
|
-
bucket_config_nickname: str | None = typer.Option(None, help="
|
|
350
|
+
bucket_config_nickname: str | None = typer.Option(None, help="Bucket config to benchmark. Defaults to the org's default bucket."),
|
|
343
351
|
):
|
|
344
352
|
"""Diagnose I/O configuration for optimal performance
|
|
345
353
|
|
|
@@ -119,6 +119,12 @@ def _parse_org_and_repo(org_and_repo: OrgAndRepoName) -> tuple[OrgName, RepoName
|
|
|
119
119
|
return (parts[0], parts[1])
|
|
120
120
|
|
|
121
121
|
|
|
122
|
+
def _repo_status_text(headline: str, status: RepoOperationStatusResponse) -> str:
|
|
123
|
+
until = f" until {status.estimated_end_time}" if status.estimated_end_time else ""
|
|
124
|
+
reason = f" {status.message}" if status.message else ""
|
|
125
|
+
return f"{headline}{until}.{reason}"
|
|
126
|
+
|
|
127
|
+
|
|
122
128
|
def _validate_service_uri(service_uri: str) -> None:
|
|
123
129
|
if not service_uri.startswith("http"):
|
|
124
130
|
raise ValueError("service uri must start with http")
|
|
@@ -732,11 +738,9 @@ class AsyncClient:
|
|
|
732
738
|
response = await mstore.open_repo(repo_name)
|
|
733
739
|
|
|
734
740
|
if response.status.mode == RepoOperationMode.OFFLINE:
|
|
735
|
-
raise ValueError(f"
|
|
741
|
+
raise ValueError(_repo_status_text(f"Repo {name} is offline", response.status))
|
|
736
742
|
elif response.status.mode == RepoOperationMode.MAINTENANCE:
|
|
737
|
-
warnings.warn(
|
|
738
|
-
f"Repository is in maintenance mode until {response.status.estimated_end_time}, do not attempt to write to the store"
|
|
739
|
-
)
|
|
743
|
+
warnings.warn(_repo_status_text(f"Repo {name} is read-only for maintenance", response.status))
|
|
740
744
|
|
|
741
745
|
return org, repo_name, response
|
|
742
746
|
|
|
@@ -1293,9 +1297,9 @@ class AsyncClient:
|
|
|
1293
1297
|
# Raise a warning about token scoping for Azure delegated credentials
|
|
1294
1298
|
if repo_model.bucket and repo_model.bucket.auth_config and repo_model.bucket.auth_config.method == "azure_credential_delegation":
|
|
1295
1299
|
warnings.warn(
|
|
1296
|
-
"
|
|
1297
|
-
"
|
|
1298
|
-
"
|
|
1300
|
+
"This bucket uses Azure credential delegation, so credentials cover the whole container, not only this repo. "
|
|
1301
|
+
"Users who can write to this repo can also write to every other blob in the container. "
|
|
1302
|
+
"Do not keep other data in this container.",
|
|
1299
1303
|
UserWarning,
|
|
1300
1304
|
)
|
|
1301
1305
|
|
|
@@ -1397,9 +1401,9 @@ class AsyncClient:
|
|
|
1397
1401
|
# Raise a warning about token scoping for Azure delegated credentials
|
|
1398
1402
|
if repo_model.bucket and repo_model.bucket.auth_config and repo_model.bucket.auth_config.method == "azure_credential_delegation":
|
|
1399
1403
|
warnings.warn(
|
|
1400
|
-
"
|
|
1401
|
-
"
|
|
1402
|
-
"
|
|
1404
|
+
"This bucket uses Azure credential delegation, so credentials cover the whole container, not only this repo. "
|
|
1405
|
+
"Users who can write to this repo can also write to every other blob in the container. "
|
|
1406
|
+
"Do not keep other data in this container.",
|
|
1403
1407
|
UserWarning,
|
|
1404
1408
|
)
|
|
1405
1409
|
|
|
@@ -1471,7 +1475,7 @@ class AsyncClient:
|
|
|
1471
1475
|
from arraylake import __version__ as arraylake_version
|
|
1472
1476
|
|
|
1473
1477
|
if not repo_model.subscription and not repo_model.bucket:
|
|
1474
|
-
raise ValueError("
|
|
1478
|
+
raise ValueError(f"Repo {repo_model.org}/{repo_model.name} has no storage bucket.")
|
|
1475
1479
|
|
|
1476
1480
|
effective_bucket = repo_model.bucket
|
|
1477
1481
|
effective_prefix = repo_model.prefix
|
|
@@ -1481,7 +1485,7 @@ class AsyncClient:
|
|
|
1481
1485
|
effective_prefix = repo_model.subscription.parent_repo.prefix
|
|
1482
1486
|
|
|
1483
1487
|
if effective_bucket is None:
|
|
1484
|
-
raise ValueError("Repo has no bucket
|
|
1488
|
+
raise ValueError(f"Repo {repo_model.org}/{repo_model.name} has no storage bucket.")
|
|
1485
1489
|
|
|
1486
1490
|
credential_refresh_func = self._maybe_get_credential_refresh_func_for_icechunk(
|
|
1487
1491
|
bucket=effective_bucket, org=repo_model.org, repo_name=repo_model.name
|
|
@@ -3129,7 +3133,7 @@ class Client:
|
|
|
3129
3133
|
Resolves the URL of the org's enabled openEO compute service and returns
|
|
3130
3134
|
an authenticated ``openeo.Connection``. Requires the optional ``openeo``
|
|
3131
3135
|
dependency (``pip install 'arraylake[openeo]'``) and an enabled openEO
|
|
3132
|
-
service (``Client().get_services(org).enable("openeo")``).
|
|
3136
|
+
service (``Client().get_services(org).enable("openeo", sku_code="s")``).
|
|
3133
3137
|
|
|
3134
3138
|
Args:
|
|
3135
3139
|
org: Name of the org.
|
|
@@ -3146,9 +3150,11 @@ class Client:
|
|
|
3146
3150
|
deployments = [d for d in self.get_services(org).list_enabled() if d.config.service_type == ServiceType.openeo]
|
|
3147
3151
|
if deployment_name is not None:
|
|
3148
3152
|
deployments = [d for d in deployments if (d.config.deployment_name or ServiceType.openeo.value) == deployment_name]
|
|
3153
|
+
if not deployments and deployment_name is not None:
|
|
3154
|
+
raise ValueError(f"Org {org!r} has no openEO service named {deployment_name!r}.")
|
|
3149
3155
|
if not deployments:
|
|
3150
3156
|
raise ValueError(
|
|
3151
|
-
f"
|
|
3157
|
+
f"Org {org!r} has no openEO service. Enable one with Client().get_services({org!r}).enable('openeo', sku_code='s')."
|
|
3152
3158
|
)
|
|
3153
3159
|
|
|
3154
3160
|
token = self.aclient.token
|
|
@@ -11,6 +11,7 @@ from pydantic import TypeAdapter
|
|
|
11
11
|
from arraylake.api_utils import ArraylakeHttpClient, handle_response
|
|
12
12
|
from arraylake.compute.types import (
|
|
13
13
|
ComputeConfig,
|
|
14
|
+
ComputeRegion,
|
|
14
15
|
DeploymentInfo,
|
|
15
16
|
LoadResults,
|
|
16
17
|
ServiceConfig,
|
|
@@ -18,6 +19,7 @@ from arraylake.compute.types import (
|
|
|
18
19
|
|
|
19
20
|
# type adapters
|
|
20
21
|
LIST_SERVICES_ADAPTER = TypeAdapter(list[DeploymentInfo])
|
|
22
|
+
LIST_REGIONS_ADAPTER = TypeAdapter(list[ComputeRegion])
|
|
21
23
|
DUMP_CONFIGS_ADAPTER = TypeAdapter(dict[str, list[ServiceConfig]])
|
|
22
24
|
|
|
23
25
|
|
|
@@ -90,6 +92,12 @@ class ComputeHttpClient(ArraylakeHttpClient):
|
|
|
90
92
|
handle_response(response)
|
|
91
93
|
return response.json()
|
|
92
94
|
|
|
95
|
+
async def list_compute_regions(self) -> list[ComputeRegion]:
|
|
96
|
+
"""Lists the regions where this organization can run compute services."""
|
|
97
|
+
response = await self._request("GET", f"/compute/orgs/{self._config.org}/regions")
|
|
98
|
+
handle_response(response)
|
|
99
|
+
return LIST_REGIONS_ADAPTER.validate_json(response.content)
|
|
100
|
+
|
|
93
101
|
async def list_compute_services(self) -> list[DeploymentInfo]:
|
|
94
102
|
"""Lists all of the Arraylake compute services for a given organization."""
|
|
95
103
|
response = await self._request("GET", f"/compute/orgs/{self._config.org}/services")
|
|
@@ -130,10 +138,13 @@ class ComputeHttpClient(ArraylakeHttpClient):
|
|
|
130
138
|
url = url + "?" + urlencode(params)
|
|
131
139
|
|
|
132
140
|
async with await self._stream_request("GET", url) as response:
|
|
141
|
+
if response.is_error:
|
|
142
|
+
await response.aread()
|
|
143
|
+
handle_response(response)
|
|
133
144
|
async for line in response.aiter_lines():
|
|
134
145
|
yield line
|
|
135
146
|
except Exception as e:
|
|
136
|
-
raise RuntimeError(f"
|
|
147
|
+
raise RuntimeError(f"Could not read logs for {service_id}: {e}") from e
|
|
137
148
|
|
|
138
149
|
async def update_compute_service(self, service_id: str, changes: dict[str, Any]) -> DeploymentInfo:
|
|
139
150
|
"""Changes a running Arraylake compute service's shape or scaling.
|
|
@@ -20,7 +20,7 @@ from typing import Any
|
|
|
20
20
|
|
|
21
21
|
from arraylake.asyn import sync
|
|
22
22
|
from arraylake.compute.http_client import ComputeHttpClient, HttpComputeConfig
|
|
23
|
-
from arraylake.compute.types import DeploymentInfo
|
|
23
|
+
from arraylake.compute.types import ComputeRegion, DeploymentInfo
|
|
24
24
|
|
|
25
25
|
_VALID_NAME = r"(\w[\w\.\-_]+)"
|
|
26
26
|
|
|
@@ -61,10 +61,18 @@ class AsyncComputeClient:
|
|
|
61
61
|
"""Get a new HTTP client for the organization."""
|
|
62
62
|
return ComputeHttpClient(HttpComputeConfig(self.service_uri, self.org, self.token))
|
|
63
63
|
|
|
64
|
-
async def enable(
|
|
64
|
+
async def enable(
|
|
65
|
+
self, protocol: str, is_public: bool = False, *, sku_code: str, region: str | None = None, **kwargs: Any
|
|
66
|
+
) -> dict[str, Any]:
|
|
65
67
|
"""Enables a new Arraylake compute service."""
|
|
68
|
+
if region is not None:
|
|
69
|
+
kwargs["region"] = region
|
|
66
70
|
return await self.http_client.create_compute_service(protocol, is_public, sku_code=sku_code, **kwargs)
|
|
67
71
|
|
|
72
|
+
async def regions(self) -> list[ComputeRegion]:
|
|
73
|
+
"""Lists the regions where the organization can run compute services."""
|
|
74
|
+
return await self.http_client.list_compute_regions()
|
|
75
|
+
|
|
68
76
|
async def list_enabled(self) -> list[DeploymentInfo]:
|
|
69
77
|
"""Lists all of the Arraylake compute services for the organization."""
|
|
70
78
|
return await self.http_client.list_compute_services()
|
|
@@ -144,7 +152,7 @@ class ComputeClient:
|
|
|
144
152
|
|
|
145
153
|
return wrap(*args, **kwargs)
|
|
146
154
|
|
|
147
|
-
def enable(self, protocol: str, is_public: bool = False, *, sku_code: str, **kwargs) -> dict[str, Any]:
|
|
155
|
+
def enable(self, protocol: str, is_public: bool = False, *, sku_code: str, region: str | None = None, **kwargs: Any) -> dict[str, Any]:
|
|
148
156
|
"""Enables a new Arraylake compute service.
|
|
149
157
|
|
|
150
158
|
Args:
|
|
@@ -152,12 +160,23 @@ class ComputeClient:
|
|
|
152
160
|
is_public: Whether the service should be public or not. Defaults to False.
|
|
153
161
|
sku_code: The compute shape to run the service on. Every service runs on a priced
|
|
154
162
|
shape, so this is required.
|
|
163
|
+
region: The region to run the service in, such as "eu-west-1". `regions()` lists the
|
|
164
|
+
choices. Defaults to the environment's default region. The region cannot change
|
|
165
|
+
after creation.
|
|
155
166
|
**kwargs: Additional arguments to pass to the service.
|
|
156
167
|
|
|
157
168
|
Returns:
|
|
158
169
|
Confirmation of the service being enabled.
|
|
159
170
|
"""
|
|
160
|
-
return self._synchronize(self._aclient.enable, protocol, is_public, sku_code=sku_code, **kwargs)
|
|
171
|
+
return self._synchronize(self._aclient.enable, protocol, is_public, sku_code=sku_code, region=region, **kwargs)
|
|
172
|
+
|
|
173
|
+
def regions(self) -> list[ComputeRegion]:
|
|
174
|
+
"""Lists the regions where the organization can run compute services.
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
The available regions. The one marked `default` is used when `enable` gets no region.
|
|
178
|
+
"""
|
|
179
|
+
return self._synchronize(self._aclient.regions)
|
|
161
180
|
|
|
162
181
|
def list_enabled(self) -> list[DeploymentInfo]:
|
|
163
182
|
"""Lists all of the Arraylake compute services for the organization.
|
|
@@ -141,6 +141,9 @@ class ServiceConfig(BaseModel):
|
|
|
141
141
|
memory_request: str | None = None
|
|
142
142
|
memory_limit: str | None = None
|
|
143
143
|
|
|
144
|
+
# Fixed at creation. To move a service, disable it and enable it again in the other region.
|
|
145
|
+
region: str | None = None
|
|
146
|
+
|
|
144
147
|
@field_validator("deployment_name")
|
|
145
148
|
@classmethod
|
|
146
149
|
def validate_deployment_name(cls, v: str | None) -> str | None:
|
|
@@ -170,6 +173,14 @@ class DeploymentInfo(BaseModel):
|
|
|
170
173
|
# Pods serving right now. A service that scales to zero sits at 0 while still being available,
|
|
171
174
|
# so this is what separates "ready to answer" from "the next request has to start it".
|
|
172
175
|
ready_replicas: int = 0
|
|
176
|
+
# An older server sends no region.
|
|
177
|
+
region: str | None = None
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
class ComputeRegion(BaseModel):
|
|
181
|
+
name: str
|
|
182
|
+
location: str | None = None
|
|
183
|
+
default: bool = False
|
|
173
184
|
|
|
174
185
|
|
|
175
186
|
class LoadResults(BaseModel):
|
|
@@ -184,9 +195,8 @@ class ComputeConfig(RedactedReprModel):
|
|
|
184
195
|
container_repository: str
|
|
185
196
|
kube_config: dict
|
|
186
197
|
legacy_domain: str | None = None
|
|
187
|
-
openmeter_api_key: str | None = None
|
|
188
198
|
|
|
189
|
-
__redacted_repr_fields__ = frozenset({"kube_config"
|
|
199
|
+
__redacted_repr_fields__ = frozenset({"kube_config"})
|
|
190
200
|
|
|
191
201
|
|
|
192
202
|
class LogMessage(BaseModel):
|
|
@@ -269,7 +269,7 @@ class HttpMetastore(ArraylakeHttpClient, Metastore):
|
|
|
269
269
|
retain_data: bool = False,
|
|
270
270
|
) -> None:
|
|
271
271
|
if not (imsure and imreallysure):
|
|
272
|
-
raise ValueError("
|
|
272
|
+
raise ValueError("Pass imsure=True and imreallysure=True to confirm that you want to delete this repo.")
|
|
273
273
|
|
|
274
274
|
# Default behavior is a soft-delete: the repo enters a recoverable
|
|
275
275
|
# "ghost" state for the grace period and the daily cleanup job reclaims
|
|
@@ -25,9 +25,12 @@ FORBIDDEN_VIRTUAL_CHUNK_PREFIXES = {"file://", "memory://", "http://", "https://
|
|
|
25
25
|
|
|
26
26
|
def _ensure_utc(expiration: datetime | None) -> datetime | None:
|
|
27
27
|
"""icechunk's Rust layer rejects a tz-naive expiration; treat naive as UTC."""
|
|
28
|
-
if expiration is
|
|
28
|
+
if expiration is None:
|
|
29
|
+
return None
|
|
30
|
+
if expiration.tzinfo is None:
|
|
29
31
|
return expiration.replace(tzinfo=UTC)
|
|
30
|
-
|
|
32
|
+
# icechunk also rejects botocore's dateutil tzutc() on fresh STS expirations.
|
|
33
|
+
return expiration.astimezone(UTC)
|
|
31
34
|
|
|
32
35
|
|
|
33
36
|
def get_icechunk_container_credentials(
|
|
@@ -174,10 +177,7 @@ def forbid_unsafe_virtual_bucket_configs(bucket: BucketResponse, bucket_nickname
|
|
|
174
177
|
|
|
175
178
|
# exclude HMAC buckets specifically as these will never be safe
|
|
176
179
|
if isinstance(auth_method, HmacAuth):
|
|
177
|
-
raise ValueError(
|
|
178
|
-
"Cannot use virtual chunk references that refer to a bucket which uses HMAC credentials, "
|
|
179
|
-
f"But bucket {bucket_nickname} uses HMAC credentials."
|
|
180
|
-
)
|
|
180
|
+
raise ValueError(f"Bucket {bucket_nickname} uses HMAC credentials, so virtual chunk references cannot point to it.")
|
|
181
181
|
|
|
182
182
|
|
|
183
183
|
def forbid_unsafe_virtual_chunk_containers(
|
|
@@ -241,8 +241,8 @@ async def tune_org(org: str, bucket_config_nickname: str | None = None) -> Optim
|
|
|
241
241
|
results = await tune_repo(repo)
|
|
242
242
|
optimal_config = OptimalIOConfig(read=results.best_read(), write=results.best_write())
|
|
243
243
|
finally:
|
|
244
|
-
with CONSOLE.status("
|
|
245
|
-
client.delete_repo(repo_name, imsure=True, imreallysure=True)
|
|
244
|
+
with CONSOLE.status(f"Deleting temporary repo [yellow]{repo_name}"):
|
|
245
|
+
client.delete_repo(repo_name, imsure=True, imreallysure=True, immediate=True)
|
|
246
246
|
CONSOLE.print("Optimal I/O configuration found:")
|
|
247
247
|
CONSOLE.print(optimal_config)
|
|
248
248
|
return optimal_config
|