arraylake 1.1.1__tar.gz → 1.2.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.1.1 → arraylake-1.2.0}/PKG-INFO +3 -1
- {arraylake-1.1.1 → arraylake-1.2.0}/arraylake/_version.py +2 -2
- {arraylake-1.1.1 → arraylake-1.2.0}/arraylake/api_utils.py +10 -6
- {arraylake-1.1.1 → arraylake-1.2.0}/arraylake/cli/repo.py +12 -1
- {arraylake-1.1.1 → arraylake-1.2.0}/arraylake/cli/skills.py +22 -13
- {arraylake-1.1.1 → arraylake-1.2.0}/arraylake/client.py +100 -9
- {arraylake-1.1.1 → arraylake-1.2.0}/arraylake/compute/types.py +22 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/arraylake/diagnostics.py +9 -5
- {arraylake-1.1.1 → arraylake-1.2.0}/arraylake/metastore/abc.py +19 -3
- {arraylake-1.1.1 → arraylake-1.2.0}/arraylake/metastore/http_metastore.py +28 -2
- {arraylake-1.1.1 → arraylake-1.2.0}/arraylake/repos/icechunk/types.py +19 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/arraylake/token.py +57 -39
- {arraylake-1.1.1 → arraylake-1.2.0}/arraylake/types.py +45 -5
- {arraylake-1.1.1 → arraylake-1.2.0}/pyproject.toml +7 -1
- {arraylake-1.1.1 → arraylake-1.2.0}/tests/compute/test_client.py +1 -1
- {arraylake-1.1.1 → arraylake-1.2.0}/tests/conftest.py +3 -3
- {arraylake-1.1.1 → arraylake-1.2.0}/tests/test_api_utils.py +35 -2
- {arraylake-1.1.1 → arraylake-1.2.0}/tests/test_cli.py +4 -3
- {arraylake-1.1.1 → arraylake-1.2.0}/tests/test_client.py +17 -18
- {arraylake-1.1.1 → arraylake-1.2.0}/tests/test_subscriptions.py +1 -1
- {arraylake-1.1.1 → arraylake-1.2.0}/tests/test_token_handler.py +82 -141
- {arraylake-1.1.1 → arraylake-1.2.0}/uv.lock +213 -47
- {arraylake-1.1.1 → arraylake-1.2.0}/.gitignore +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/README.md +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/arraylake/__init__.py +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/arraylake/__main__.py +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/arraylake/_credential_cache.py +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/arraylake/asyn.py +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/arraylake/cli/__init__.py +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/arraylake/cli/auth.py +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/arraylake/cli/compute.py +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/arraylake/cli/config.py +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/arraylake/cli/main.py +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/arraylake/cli/utils.py +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/arraylake/compute/doctor.py +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/arraylake/compute/http_client.py +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/arraylake/compute/services.py +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/arraylake/config.py +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/arraylake/config.yaml +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/arraylake/credentials.py +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/arraylake/display/__init__.py +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/arraylake/display/repo.py +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/arraylake/display/repolist.py +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/arraylake/exceptions.py +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/arraylake/log_util.py +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/arraylake/metastore/__init__.py +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/arraylake/py.typed +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/arraylake/repos/__init__.py +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/arraylake/repos/icechunk/__init__.py +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/arraylake/repos/icechunk/storage.py +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/arraylake/repos/icechunk/virtual.py +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/arraylake/tuning.py +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/tests/__init__.py +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/tests/compute/test_doctor.py +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/tests/config.yaml +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/tests/icechunk/__init__.py +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/tests/icechunk/conftest.py +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/tests/icechunk/test_storage.py +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/tests/icechunk/test_virtual.py +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/tests/test_asyn.py +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/tests/test_config.py +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/tests/test_credential_cache.py +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/tests/test_credentials.py +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/tests/test_diagnostics.py +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/tests/test_performance.py +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/tests/test_skills.py +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/tests/test_types.py +0 -0
- {arraylake-1.1.1 → arraylake-1.2.0}/tests/test_virtual_chunks.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: arraylake
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.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
|
|
@@ -29,6 +29,8 @@ Requires-Dist: cf-xarray>=0.10.4; extra == 'maximal'
|
|
|
29
29
|
Requires-Dist: xarray[accel,etc,io,parallel]>=2024.10.0; extra == 'maximal'
|
|
30
30
|
Provides-Extra: obstore
|
|
31
31
|
Requires-Dist: obstore>=0.9.2; extra == 'obstore'
|
|
32
|
+
Provides-Extra: openeo
|
|
33
|
+
Requires-Dist: openeo<0.40,>=0.35.0; extra == 'openeo'
|
|
32
34
|
Provides-Extra: xarray
|
|
33
35
|
Requires-Dist: cf-xarray>=0.10.4; extra == 'xarray'
|
|
34
36
|
Requires-Dist: xarray>=2024.10.0; extra == 'xarray'
|
|
@@ -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.2.0'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 2, 0)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -542,17 +542,21 @@ class UserAuth(httpx.Auth):
|
|
|
542
542
|
return f"Bearer {token}"
|
|
543
543
|
|
|
544
544
|
async def async_auth_flow(self, request: httpx.Request) -> AsyncGenerator[httpx.Request, httpx.Response]:
|
|
545
|
-
|
|
545
|
+
token_at_send = self._bearer_token
|
|
546
|
+
request.headers["Authorization"] = token_at_send
|
|
546
547
|
response = yield request
|
|
547
548
|
if response.status_code == httpx.codes.UNAUTHORIZED:
|
|
548
549
|
# If the server issues a 401 response, then issue a request to
|
|
549
550
|
# refresh tokens, and resend the request.
|
|
550
551
|
async with self._async_lock:
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
552
|
+
# A concurrently-401'd request may have refreshed while we waited for
|
|
553
|
+
# the lock; refreshing again would burn a redundant Auth0 rotation.
|
|
554
|
+
if self._bearer_token == token_at_send:
|
|
555
|
+
refresh_response = yield await self._token_handler.build_refresh_request()
|
|
556
|
+
await refresh_response.aread()
|
|
557
|
+
handle_response(refresh_response)
|
|
558
|
+
new_tokens = OauthTokensResponse.model_validate_json(refresh_response.content)
|
|
559
|
+
self._token_handler.update(new_tokens)
|
|
556
560
|
|
|
557
561
|
request.headers["Authorization"] = self._bearer_token
|
|
558
562
|
yield request
|
|
@@ -239,9 +239,14 @@ async def modify(
|
|
|
239
239
|
async def delete(
|
|
240
240
|
repo_name: str = typer.Argument(..., help="Name of repository {ORG}/{REPO_NAME}"),
|
|
241
241
|
confirm: bool = typer.Option(False, help="confirm deletion without prompting"),
|
|
242
|
+
immediate: bool = typer.Option(False, help="skip the recovery grace period and move straight to cleanup (cannot be restored)"),
|
|
243
|
+
retain_data: bool = typer.Option(False, help="leave the bucket bytes in place; only remove Arraylake metadata"),
|
|
242
244
|
):
|
|
243
245
|
"""**Delete** a repository
|
|
244
246
|
|
|
247
|
+
By default the repo enters a recoverable state for a grace period before its
|
|
248
|
+
data is cleaned up. Pass --immediate to skip the grace period.
|
|
249
|
+
|
|
245
250
|
**Examples**
|
|
246
251
|
|
|
247
252
|
- Delete repository without confirmation prompt
|
|
@@ -249,6 +254,12 @@ async def delete(
|
|
|
249
254
|
```
|
|
250
255
|
$ arraylake repo delete my-org/example-repo --confirm
|
|
251
256
|
```
|
|
257
|
+
|
|
258
|
+
- Delete immediately, freeing the name right away
|
|
259
|
+
|
|
260
|
+
```
|
|
261
|
+
$ arraylake repo delete my-org/example-repo --confirm --immediate
|
|
262
|
+
```
|
|
252
263
|
"""
|
|
253
264
|
if not confirm:
|
|
254
265
|
confirm = typer.confirm(
|
|
@@ -259,7 +270,7 @@ async def delete(
|
|
|
259
270
|
client = AsyncClient()
|
|
260
271
|
|
|
261
272
|
with simple_progress(f"Deleting repo [bold]{repo_name}[/bold]..."):
|
|
262
|
-
await client.delete_repo(repo_name, imsure=confirm, imreallysure=confirm)
|
|
273
|
+
await client.delete_repo(repo_name, imsure=confirm, imreallysure=confirm, immediate=immediate, retain_data=retain_data)
|
|
263
274
|
|
|
264
275
|
# If the repo is a icechunk repo, print message that the bucket must be deleted manually
|
|
265
276
|
rich_console.print(f"Repo [bold]{repo_name}[/bold] removed from Arraylake. \nThe underlying Icechunk bucket must be deleted manually.")
|
|
@@ -18,6 +18,7 @@ required.
|
|
|
18
18
|
|
|
19
19
|
from __future__ import annotations
|
|
20
20
|
|
|
21
|
+
import asyncio
|
|
21
22
|
import shutil
|
|
22
23
|
import tempfile
|
|
23
24
|
from dataclasses import dataclass
|
|
@@ -81,14 +82,18 @@ def _parse_manifest(payload: dict) -> list[SkillEntry]:
|
|
|
81
82
|
return entries
|
|
82
83
|
|
|
83
84
|
|
|
84
|
-
def
|
|
85
|
+
async def _async_fetch_manifest(uri: str) -> list[SkillEntry]:
|
|
85
86
|
url = f"{uri}/manifest.json"
|
|
86
|
-
with httpx.
|
|
87
|
-
resp = client.get(url)
|
|
87
|
+
async with httpx.AsyncClient(timeout=20.0, follow_redirects=True) as client:
|
|
88
|
+
resp = await client.get(url)
|
|
88
89
|
resp.raise_for_status()
|
|
89
90
|
return _parse_manifest(resp.json())
|
|
90
91
|
|
|
91
92
|
|
|
93
|
+
def _fetch_manifest(uri: str) -> list[SkillEntry]:
|
|
94
|
+
return asyncio.run(_async_fetch_manifest(uri))
|
|
95
|
+
|
|
96
|
+
|
|
92
97
|
def _empty_temporary_sibling(dest: Path, prefix: str) -> Path:
|
|
93
98
|
path = Path(tempfile.mkdtemp(prefix=prefix, dir=dest))
|
|
94
99
|
path.rmdir()
|
|
@@ -102,6 +107,19 @@ def _remove_path(path: Path) -> None:
|
|
|
102
107
|
shutil.rmtree(path)
|
|
103
108
|
|
|
104
109
|
|
|
110
|
+
async def _download_skill_files(uri: str, entry: SkillEntry, tmp_root: Path) -> None:
|
|
111
|
+
async with httpx.AsyncClient(timeout=30.0, follow_redirects=True) as client:
|
|
112
|
+
for rel in entry.files:
|
|
113
|
+
if ".." in rel.split("/") or rel.startswith("/"):
|
|
114
|
+
raise RuntimeError(f"Refusing to write file outside skill root: {rel}")
|
|
115
|
+
file_url = f"{uri}/{entry.name}/{rel}"
|
|
116
|
+
resp = await client.get(file_url)
|
|
117
|
+
resp.raise_for_status()
|
|
118
|
+
target = tmp_root / rel
|
|
119
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
120
|
+
target.write_bytes(resp.content)
|
|
121
|
+
|
|
122
|
+
|
|
105
123
|
def _install_skill(uri: str, entry: SkillEntry, dest: Path) -> str:
|
|
106
124
|
"""Download all of `entry.files` into `dest/<name>/...`.
|
|
107
125
|
|
|
@@ -115,16 +133,7 @@ def _install_skill(uri: str, entry: SkillEntry, dest: Path) -> str:
|
|
|
115
133
|
backup_root: Path | None = None
|
|
116
134
|
|
|
117
135
|
try:
|
|
118
|
-
|
|
119
|
-
for rel in entry.files:
|
|
120
|
-
if ".." in rel.split("/") or rel.startswith("/"):
|
|
121
|
-
raise RuntimeError(f"Refusing to write file outside skill root: {rel}")
|
|
122
|
-
file_url = f"{uri}/{entry.name}/{rel}"
|
|
123
|
-
resp = client.get(file_url)
|
|
124
|
-
resp.raise_for_status()
|
|
125
|
-
target = tmp_root / rel
|
|
126
|
-
target.parent.mkdir(parents=True, exist_ok=True)
|
|
127
|
-
target.write_bytes(resp.content)
|
|
136
|
+
asyncio.run(_download_skill_files(uri, entry, tmp_root))
|
|
128
137
|
|
|
129
138
|
if skill_root.exists() or skill_root.is_symlink():
|
|
130
139
|
# Pre-existing install, including legacy wheel-bundled symlinks.
|
|
@@ -29,6 +29,7 @@ from icechunk import Repository as IcechunkRepository
|
|
|
29
29
|
from arraylake._credential_cache import CredentialCacheKey, get_or_refresh, populate
|
|
30
30
|
from arraylake.asyn import async_gather_tasks, asyncio_run, sync
|
|
31
31
|
from arraylake.compute.services import AsyncComputeClient, ComputeClient
|
|
32
|
+
from arraylake.compute.types import ServiceType
|
|
32
33
|
from arraylake.config import config as arraylake_config
|
|
33
34
|
from arraylake.config import default_service_uri
|
|
34
35
|
from arraylake.credentials import (
|
|
@@ -90,6 +91,7 @@ from arraylake.types import Repo as RepoModel
|
|
|
90
91
|
|
|
91
92
|
if TYPE_CHECKING:
|
|
92
93
|
import obstore as obs
|
|
94
|
+
import openeo
|
|
93
95
|
|
|
94
96
|
logger = get_logger(__name__)
|
|
95
97
|
|
|
@@ -1061,7 +1063,7 @@ class AsyncClient:
|
|
|
1061
1063
|
|
|
1062
1064
|
except (IcechunkError, ValueError):
|
|
1063
1065
|
# If the repo fails to create, we need to delete the repo model
|
|
1064
|
-
await mstore.delete_database(repo_name, imsure=True, imreallysure=True)
|
|
1066
|
+
await mstore.delete_database(repo_name, imsure=True, imreallysure=True, immediate=True, retain_data=True)
|
|
1065
1067
|
raise
|
|
1066
1068
|
|
|
1067
1069
|
async def import_repo(
|
|
@@ -1160,7 +1162,7 @@ class AsyncClient:
|
|
|
1160
1162
|
|
|
1161
1163
|
except (IcechunkError, ValueError):
|
|
1162
1164
|
# If the repo fails to create, we need to delete the repo model
|
|
1163
|
-
await mstore.delete_database(repo_name, imsure=True, imreallysure=True)
|
|
1165
|
+
await mstore.delete_database(repo_name, imsure=True, imreallysure=True, immediate=True, retain_data=True)
|
|
1164
1166
|
raise
|
|
1165
1167
|
|
|
1166
1168
|
async def _get_icechunk_storage_from_repo_model(
|
|
@@ -1466,16 +1468,38 @@ class AsyncClient:
|
|
|
1466
1468
|
optimization_config=optimization_config,
|
|
1467
1469
|
)
|
|
1468
1470
|
|
|
1469
|
-
async def delete_repo(
|
|
1470
|
-
|
|
1471
|
+
async def delete_repo(
|
|
1472
|
+
self,
|
|
1473
|
+
name: OrgAndRepoName,
|
|
1474
|
+
*,
|
|
1475
|
+
imsure: bool = False,
|
|
1476
|
+
imreallysure: bool = False,
|
|
1477
|
+
immediate: bool = False,
|
|
1478
|
+
retain_data: bool = False,
|
|
1479
|
+
) -> None:
|
|
1480
|
+
"""Delete a repo.
|
|
1481
|
+
|
|
1482
|
+
By default the repo enters a recoverable "ghost" state for a grace
|
|
1483
|
+
period (currently 7 days) and the bucket bytes are reclaimed by the
|
|
1484
|
+
daily cleanup job after that. A repo in the ghost state can be brought
|
|
1485
|
+
back through the web app.
|
|
1471
1486
|
|
|
1472
1487
|
Args:
|
|
1473
1488
|
name: Full name of the repo to delete (of the form [ORG]/[REPO])
|
|
1474
|
-
imsure, imreallysure: confirm you intend to delete this
|
|
1489
|
+
imsure, imreallysure: confirm you intend to delete this repo
|
|
1490
|
+
immediate: skip the grace period and move straight to cleanup
|
|
1491
|
+
retain_data: leave the bucket bytes alone; only remove Arraylake
|
|
1492
|
+
metadata
|
|
1475
1493
|
"""
|
|
1476
1494
|
org, repo_name = _parse_org_and_repo(name)
|
|
1477
1495
|
mstore = self._metastore_for_org(org)
|
|
1478
|
-
await mstore.delete_database(
|
|
1496
|
+
await mstore.delete_database(
|
|
1497
|
+
repo_name,
|
|
1498
|
+
imsure=imsure,
|
|
1499
|
+
imreallysure=imreallysure,
|
|
1500
|
+
immediate=immediate,
|
|
1501
|
+
retain_data=retain_data,
|
|
1502
|
+
)
|
|
1479
1503
|
|
|
1480
1504
|
async def _set_repo_status(
|
|
1481
1505
|
self, qualified_repo_name: OrgAndRepoName, mode: RepoOperationMode, message: str | None = None
|
|
@@ -2239,14 +2263,38 @@ class Client:
|
|
|
2239
2263
|
optimization_config=optimization_config,
|
|
2240
2264
|
)
|
|
2241
2265
|
|
|
2242
|
-
def delete_repo(
|
|
2243
|
-
|
|
2266
|
+
def delete_repo(
|
|
2267
|
+
self,
|
|
2268
|
+
name: OrgAndRepoName,
|
|
2269
|
+
*,
|
|
2270
|
+
imsure: bool = False,
|
|
2271
|
+
imreallysure: bool = False,
|
|
2272
|
+
immediate: bool = False,
|
|
2273
|
+
retain_data: bool = False,
|
|
2274
|
+
) -> None:
|
|
2275
|
+
"""Delete a repo.
|
|
2276
|
+
|
|
2277
|
+
By default the repo enters a recoverable "ghost" state for a grace
|
|
2278
|
+
period (currently 7 days) and the bucket bytes are reclaimed by the
|
|
2279
|
+
daily cleanup job after that. A repo in the ghost state can be brought
|
|
2280
|
+
back through the web app.
|
|
2244
2281
|
|
|
2245
2282
|
Args:
|
|
2246
2283
|
name: Full name of the repo to delete (of the form [ORG]/[REPO])
|
|
2284
|
+
imsure, imreallysure: confirm you intend to delete this repo
|
|
2285
|
+
immediate: skip the grace period and move straight to cleanup
|
|
2286
|
+
retain_data: leave the bucket bytes alone; only remove Arraylake
|
|
2287
|
+
metadata
|
|
2247
2288
|
"""
|
|
2248
2289
|
|
|
2249
|
-
return sync(
|
|
2290
|
+
return sync(
|
|
2291
|
+
self.aclient.delete_repo,
|
|
2292
|
+
name,
|
|
2293
|
+
imsure=imsure,
|
|
2294
|
+
imreallysure=imreallysure,
|
|
2295
|
+
immediate=immediate,
|
|
2296
|
+
retain_data=retain_data,
|
|
2297
|
+
)
|
|
2250
2298
|
|
|
2251
2299
|
def create_bucket_config(
|
|
2252
2300
|
self,
|
|
@@ -2435,3 +2483,46 @@ class Client:
|
|
|
2435
2483
|
org: Name of the org
|
|
2436
2484
|
"""
|
|
2437
2485
|
return self.aclient.get_services(org).to_sync_client()
|
|
2486
|
+
|
|
2487
|
+
def get_openeo_connection(self, org: OrgName, deployment_name: str | None = None) -> openeo.Connection:
|
|
2488
|
+
"""Connect to this org's deployed openEO service with the openeo client.
|
|
2489
|
+
|
|
2490
|
+
Resolves the URL of the org's enabled openEO compute service and returns
|
|
2491
|
+
an authenticated ``openeo.Connection``. Requires the optional ``openeo``
|
|
2492
|
+
dependency (``pip install 'arraylake[openeo]'``) and an enabled openEO
|
|
2493
|
+
service (``Client().get_services(org).enable("openeo")``).
|
|
2494
|
+
|
|
2495
|
+
Args:
|
|
2496
|
+
org: Name of the org.
|
|
2497
|
+
deployment_name: Select a specific openEO deployment by name when the
|
|
2498
|
+
org has more than one. Defaults to the only/first openEO service.
|
|
2499
|
+
"""
|
|
2500
|
+
try:
|
|
2501
|
+
import openeo
|
|
2502
|
+
except ImportError as e:
|
|
2503
|
+
raise ImportError(
|
|
2504
|
+
"The 'openeo' package is required for get_openeo_connection. Install it with `pip install 'arraylake[openeo]'`."
|
|
2505
|
+
) from e
|
|
2506
|
+
|
|
2507
|
+
deployments = [d for d in self.get_services(org).list_enabled() if d.config.service_type == ServiceType.openeo]
|
|
2508
|
+
if deployment_name is not None:
|
|
2509
|
+
deployments = [d for d in deployments if (d.config.deployment_name or ServiceType.openeo.value) == deployment_name]
|
|
2510
|
+
if not deployments:
|
|
2511
|
+
raise ValueError(
|
|
2512
|
+
f"No openEO service is enabled for org {org!r}. Enable one with Client().get_services({org!r}).enable('openeo')."
|
|
2513
|
+
)
|
|
2514
|
+
|
|
2515
|
+
token = self.aclient.token
|
|
2516
|
+
if not token:
|
|
2517
|
+
handler = get_auth_handler(self.aclient.service_uri)
|
|
2518
|
+
if handler.tokens is not None:
|
|
2519
|
+
token = handler.tokens.id_token.get_secret_value()
|
|
2520
|
+
if not token:
|
|
2521
|
+
raise ValueError(
|
|
2522
|
+
"An Arraylake API token is required to authenticate to openEO. "
|
|
2523
|
+
"Provide one via Client(token=...) or log in with `arraylake auth login`."
|
|
2524
|
+
)
|
|
2525
|
+
|
|
2526
|
+
connection = openeo.connect(deployments[0].url)
|
|
2527
|
+
connection.authenticate_bearer_token(token)
|
|
2528
|
+
return connection
|
|
@@ -11,6 +11,7 @@ class ServiceType(StrEnum):
|
|
|
11
11
|
wms = "wms"
|
|
12
12
|
tiles = "tiles"
|
|
13
13
|
zarr = "zarr"
|
|
14
|
+
openeo = "openeo"
|
|
14
15
|
|
|
15
16
|
|
|
16
17
|
class ServiceStatus(StrEnum):
|
|
@@ -28,6 +29,23 @@ class LogLevel(StrEnum):
|
|
|
28
29
|
critical = "CRITICAL"
|
|
29
30
|
|
|
30
31
|
|
|
32
|
+
class InstanceSize(StrEnum):
|
|
33
|
+
xsmall = "xsmall"
|
|
34
|
+
small = "small"
|
|
35
|
+
medium = "medium"
|
|
36
|
+
large = "large"
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Mapping of instance sizes to resource specifications
|
|
40
|
+
# Based on AWS c7g instance family
|
|
41
|
+
INSTANCE_SIZE_SPECS: dict[InstanceSize, dict[str, int]] = {
|
|
42
|
+
InstanceSize.xsmall: {"vcpus": 1, "memory_gib": 2}, # c7g.medium
|
|
43
|
+
InstanceSize.small: {"vcpus": 4, "memory_gib": 8}, # c7g.xlarge
|
|
44
|
+
InstanceSize.medium: {"vcpus": 16, "memory_gib": 32}, # c7g.4xlarge
|
|
45
|
+
InstanceSize.large: {"vcpus": 64, "memory_gib": 128}, # c7g.16xlarge
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
|
|
31
49
|
class ServiceConfig(BaseModel):
|
|
32
50
|
service_type: ServiceType
|
|
33
51
|
org: str
|
|
@@ -83,6 +101,10 @@ class ServiceConfig(BaseModel):
|
|
|
83
101
|
# other than DAP2. DAP2 always uses Dask regardless of this setting.
|
|
84
102
|
use_dask: bool = False
|
|
85
103
|
|
|
104
|
+
# Optional instance size selection. When set, this maps to predefined
|
|
105
|
+
# cpu_request, cpu_limit, memory_request, and memory_limit values.
|
|
106
|
+
instance_size: InstanceSize | None = None
|
|
107
|
+
|
|
86
108
|
# Optional container resource overrides. When unset, the per-service-type
|
|
87
109
|
# defaults from the server's ImageConfig are used. Setting any of these
|
|
88
110
|
# requires the caller to have the CAN_MANAGE_ALL_ORGS service permission;
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
# limitations under the License.
|
|
18
18
|
"""Utility functions for printing version information."""
|
|
19
19
|
|
|
20
|
+
import asyncio
|
|
20
21
|
import importlib
|
|
21
22
|
import locale
|
|
22
23
|
import os
|
|
@@ -107,12 +108,15 @@ def get_versions() -> dict[str, str]:
|
|
|
107
108
|
return versions
|
|
108
109
|
|
|
109
110
|
|
|
110
|
-
def
|
|
111
|
-
with httpx.
|
|
112
|
-
response = client.get(service_uri)
|
|
111
|
+
async def _async_get_service_info(service_uri: str) -> dict[str, str]:
|
|
112
|
+
async with httpx.AsyncClient() as client:
|
|
113
|
+
response = await client.get(service_uri)
|
|
113
114
|
data = response.json()
|
|
114
|
-
|
|
115
|
-
|
|
115
|
+
return {"service_uri": service_uri, "service_version": data["arraylake_api_version"]}
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def get_service_info(service_uri: str) -> dict[str, str]:
|
|
119
|
+
return asyncio.run(_async_get_service_info(service_uri))
|
|
116
120
|
|
|
117
121
|
|
|
118
122
|
def get_config_info() -> dict[str, str]:
|
|
@@ -32,14 +32,30 @@ class Metastore(ABC): # pragma: no cover
|
|
|
32
32
|
...
|
|
33
33
|
|
|
34
34
|
@abstractmethod
|
|
35
|
-
async def delete_database(
|
|
35
|
+
async def delete_database(
|
|
36
|
+
self,
|
|
37
|
+
name: str,
|
|
38
|
+
*,
|
|
39
|
+
imsure: bool = False,
|
|
40
|
+
imreallysure: bool = False,
|
|
41
|
+
immediate: bool = False,
|
|
42
|
+
retain_data: bool = False,
|
|
43
|
+
) -> None:
|
|
36
44
|
"""Delete an existing metastore database.
|
|
37
45
|
|
|
38
46
|
Parameters
|
|
39
47
|
----------
|
|
40
48
|
name : str
|
|
41
49
|
Name of repo
|
|
42
|
-
imsure,
|
|
43
|
-
Confirm
|
|
50
|
+
imsure, imreallysure : bool
|
|
51
|
+
Confirm intent to delete.
|
|
52
|
+
immediate : bool, default False
|
|
53
|
+
Skip the soft-delete grace period and move straight to cleanup.
|
|
54
|
+
With the default ``immediate=False`` the repo enters a recoverable
|
|
55
|
+
"ghost" state for the grace period before the daily cleanup job
|
|
56
|
+
takes it.
|
|
57
|
+
retain_data : bool, default False
|
|
58
|
+
Leave the bucket bytes alone when the cleanup runs; only the
|
|
59
|
+
Arraylake metadata is removed.
|
|
44
60
|
"""
|
|
45
61
|
...
|
|
@@ -234,11 +234,37 @@ class HttpMetastore(ArraylakeHttpClient, Metastore):
|
|
|
234
234
|
response = await self._request("PATCH", f"/orgs/{self._config.org}/{name}", content=repo_modify_request.model_dump_json())
|
|
235
235
|
handle_response(response)
|
|
236
236
|
|
|
237
|
-
async def delete_database(
|
|
237
|
+
async def delete_database(
|
|
238
|
+
self,
|
|
239
|
+
name: str,
|
|
240
|
+
*,
|
|
241
|
+
imsure: bool = False,
|
|
242
|
+
imreallysure: bool = False,
|
|
243
|
+
immediate: bool = False,
|
|
244
|
+
retain_data: bool = False,
|
|
245
|
+
) -> None:
|
|
238
246
|
if not (imsure and imreallysure):
|
|
239
247
|
raise ValueError("Don't do this unless you're really sure. Once the database has been deleted, it's gone forever.")
|
|
240
248
|
|
|
241
|
-
|
|
249
|
+
# Default behavior is a soft-delete: the repo enters a recoverable
|
|
250
|
+
# "ghost" state for the grace period and the daily cleanup job reclaims
|
|
251
|
+
# the bucket bytes afterward. Callers can opt out:
|
|
252
|
+
# immediate=True - skip the grace period; ghost vanishes immediately
|
|
253
|
+
# and the subscription cascade runs now
|
|
254
|
+
# retain_data=True - leave bucket bytes alone when the cleanup job
|
|
255
|
+
# finalizes (only the Arraylake metadata is removed)
|
|
256
|
+
# Setting both together makes the server drop the row inline so the
|
|
257
|
+
# (org, name) slot is immediately reusable.
|
|
258
|
+
params: dict[str, str] = {}
|
|
259
|
+
if immediate:
|
|
260
|
+
params["immediate"] = "true"
|
|
261
|
+
if retain_data:
|
|
262
|
+
params["retain_data"] = "true"
|
|
263
|
+
response = await self._request(
|
|
264
|
+
"DELETE",
|
|
265
|
+
f"/orgs/{self._config.org}/{name}",
|
|
266
|
+
params=params,
|
|
267
|
+
)
|
|
242
268
|
handle_response(response)
|
|
243
269
|
|
|
244
270
|
async def create_bucket_config(self, bucket_config: NewBucket) -> BucketResponse:
|
|
@@ -98,6 +98,25 @@ class CreateBranchRequest(BaseModel):
|
|
|
98
98
|
commit_id: CommitId
|
|
99
99
|
|
|
100
100
|
|
|
101
|
+
class AttributeEdit(BaseModel):
|
|
102
|
+
"""A merge patch for one node's attributes (path "" targets the root group)."""
|
|
103
|
+
|
|
104
|
+
path: str = ""
|
|
105
|
+
attributes: dict[str, Any]
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class UpdateNodeAttributesRequest(BaseModel):
|
|
109
|
+
"""Merge attributes into one or more nodes (groups or arrays), applying every
|
|
110
|
+
edit to a branch as a single commit."""
|
|
111
|
+
|
|
112
|
+
branch: BranchName
|
|
113
|
+
edits: list[AttributeEdit]
|
|
114
|
+
message: str | None = None
|
|
115
|
+
# Optimistic concurrency: the commit the caller expects the new commit to be
|
|
116
|
+
# parented on (i.e. the current branch tip). Reject if the branch has moved.
|
|
117
|
+
expected_parent_commit_id: CommitId | None = None
|
|
118
|
+
|
|
119
|
+
|
|
101
120
|
class Tag(BaseModel):
|
|
102
121
|
id: TagName
|
|
103
122
|
commit_id: CommitId
|