arraylake 1.0.0__tar.gz → 1.0.1__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.0.0 → arraylake-1.0.1}/.gitignore +3 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/PKG-INFO +3 -3
- {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/_version.py +2 -2
- {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/api_utils.py +16 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/cli/auth.py +1 -8
- {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/client.py +9 -8
- {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/types.py +53 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/pyproject.toml +13 -14
- {arraylake-1.0.0 → arraylake-1.0.1}/tests/test_api_utils.py +19 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/uv.lock +427 -317
- {arraylake-1.0.0 → arraylake-1.0.1}/README.md +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/__init__.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/__main__.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/asyn.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/cli/__init__.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/cli/compute.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/cli/config.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/cli/main.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/cli/repo.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/cli/utils.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/compute/doctor.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/compute/http_client.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/compute/services.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/compute/types.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/config.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/config.yaml +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/credentials.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/diagnostics.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/display/__init__.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/display/repo.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/display/repolist.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/exceptions.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/log_util.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/metastore/__init__.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/metastore/abc.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/metastore/http_metastore.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/py.typed +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/repos/__init__.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/repos/icechunk/__init__.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/repos/icechunk/storage.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/repos/icechunk/types.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/repos/icechunk/virtual.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/token.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/tuning.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/tests/__init__.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/tests/compute/test_client.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/tests/compute/test_doctor.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/tests/config.yaml +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/tests/conftest.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/tests/icechunk/__init__.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/tests/icechunk/conftest.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/tests/icechunk/test_storage.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/tests/icechunk/test_virtual.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/tests/test_asyn.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/tests/test_cli.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/tests/test_client.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/tests/test_config.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/tests/test_credentials.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/tests/test_diagnostics.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/tests/test_performance.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/tests/test_subscriptions.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/tests/test_token_handler.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/tests/test_types.py +0 -0
- {arraylake-1.0.0 → arraylake-1.0.1}/tests/test_virtual_chunks.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: arraylake
|
|
3
|
-
Version: 1.0.
|
|
3
|
+
Version: 1.0.1
|
|
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
|
|
@@ -16,10 +16,10 @@ 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.0.1
|
|
18
18
|
Requires-Dist: numpy<3.0,>=1.23
|
|
19
|
-
Requires-Dist: packaging<
|
|
19
|
+
Requires-Dist: packaging<27.0,>=23.0
|
|
20
20
|
Requires-Dist: pydantic[email]<3.0,>=2.9
|
|
21
21
|
Requires-Dist: python-dateutil<3.0,>=2.8
|
|
22
|
-
Requires-Dist: rich<
|
|
22
|
+
Requires-Dist: rich<16.0,>=12.6
|
|
23
23
|
Requires-Dist: ruamel-yaml<1.0,>=0.17
|
|
24
24
|
Requires-Dist: structlog<26.0,>=24.1
|
|
25
25
|
Requires-Dist: typer<1.0,>=0.12.0
|
|
@@ -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.0.
|
|
22
|
-
__version_tuple__ = version_tuple = (1, 0,
|
|
21
|
+
__version__ = version = '1.0.1'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 0, 1)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import functools
|
|
4
5
|
import json
|
|
5
6
|
import os
|
|
6
7
|
import random
|
|
8
|
+
import uuid
|
|
7
9
|
import warnings
|
|
8
10
|
import weakref
|
|
9
11
|
from collections.abc import AsyncGenerator, Callable, Generator, Mapping
|
|
@@ -82,6 +84,19 @@ def _get_proxy_from_env() -> str | None:
|
|
|
82
84
|
return None
|
|
83
85
|
|
|
84
86
|
|
|
87
|
+
@functools.cache
|
|
88
|
+
def _get_session_id() -> str:
|
|
89
|
+
"""Session ID for this Python process.
|
|
90
|
+
|
|
91
|
+
Propagated as the ``arraylake-session-id`` header on every outbound request and
|
|
92
|
+
baked into ``ArraylakeHttpClient._default_headers`` at construction time, so it
|
|
93
|
+
rides along to dask workers via the existing pickle path on the client.
|
|
94
|
+
"""
|
|
95
|
+
session_id = str(uuid.uuid4())
|
|
96
|
+
logger.info("arraylake session id: %s", session_id)
|
|
97
|
+
return session_id
|
|
98
|
+
|
|
99
|
+
|
|
85
100
|
# this is a hashable key to use for the global client cache
|
|
86
101
|
# we only need one client for each key
|
|
87
102
|
@dataclass(eq=True, frozen=True)
|
|
@@ -302,6 +317,7 @@ class ArraylakeHttpClient:
|
|
|
302
317
|
# technically we don't enforce this, for now it's for debugging purposes
|
|
303
318
|
"client-name": "arraylake-python-client",
|
|
304
319
|
"client-version": arraylake.__version__,
|
|
320
|
+
"arraylake-session-id": _get_session_id(),
|
|
305
321
|
}
|
|
306
322
|
|
|
307
323
|
self._async_lock = asyncio.Lock()
|
|
@@ -101,14 +101,7 @@ def token():
|
|
|
101
101
|
raise typer.Exit(code=1)
|
|
102
102
|
try:
|
|
103
103
|
token_value = handler.tokens.id_token.get_secret_value()
|
|
104
|
-
|
|
105
|
-
Panel(
|
|
106
|
-
f"[dim]{token_value}[/dim]",
|
|
107
|
-
title="id_token",
|
|
108
|
-
subtitle=f"[link=https://jwt.io/#id_token={token_value}][blue]:link: Inspect with JWT.io[/blue][/link]",
|
|
109
|
-
subtitle_align="right",
|
|
110
|
-
)
|
|
111
|
-
)
|
|
104
|
+
typer.echo(token_value)
|
|
112
105
|
except (AuthException, AttributeError):
|
|
113
106
|
rich_console.print(Align(f"[yellow]{NOT_LOGGED_IN_MESSAGE}[/yellow]", align="center"))
|
|
114
107
|
raise typer.Exit(code=1)
|
|
@@ -558,16 +558,17 @@ class AsyncClient:
|
|
|
558
558
|
if response.virtual_chunk_credentials:
|
|
559
559
|
credentials_map: dict[str, icechunk.AnyS3Credential | icechunk.AnyGcsCredential | icechunk.AnyAzureCredential | None] = {}
|
|
560
560
|
for vcc_prefix, vcc_creds in response.virtual_chunk_credentials.items():
|
|
561
|
-
#
|
|
561
|
+
# Create VCC objects for all server-provided VCCs, not just runtime ones,
|
|
562
|
+
# because icechunk's config merge replaces virtual_chunk_containers
|
|
563
|
+
# rather than extending them.
|
|
564
|
+
store_config = vcc_creds.to_vcc_store_config(user_id=UUID(response.principal_id))
|
|
565
|
+
container = icechunk.VirtualChunkContainer(
|
|
566
|
+
url_prefix=vcc_prefix,
|
|
567
|
+
store=store_config,
|
|
568
|
+
)
|
|
562
569
|
if vcc_creds.runtime_vcc_name is not None:
|
|
563
|
-
bucket_config = await self.get_bucket_config(org=vcc_creds.org, nickname=vcc_creds.bucket_nickname)
|
|
564
|
-
store_config = bucket_config.to_vcc_store_config(user_id=UUID(response.principal_id))
|
|
565
|
-
container = icechunk.VirtualChunkContainer(
|
|
566
|
-
url_prefix=vcc_prefix,
|
|
567
|
-
store=store_config,
|
|
568
|
-
)
|
|
569
570
|
container.name = vcc_creds.runtime_vcc_name
|
|
570
|
-
|
|
571
|
+
config.set_virtual_chunk_container(container)
|
|
571
572
|
|
|
572
573
|
# TODO: build refresh closures here using vcc_creds.org, vcc_creds.bucket_nickname,
|
|
573
574
|
# and vcc_creds.platform so that credentials can be refreshed when they expire. (See https://github.com/earth-mover/arraylake/issues/5122)
|
|
@@ -1232,6 +1232,10 @@ class VirtualChunkCredentials(BaseModel):
|
|
|
1232
1232
|
|
|
1233
1233
|
The org, bucket_nickname, and platform fields are needed so the client can
|
|
1234
1234
|
construct credential refresh closures for delegated-creds buckets.
|
|
1235
|
+
|
|
1236
|
+
The store config fields (region, endpoint_url, allow_http, force_path_style)
|
|
1237
|
+
allow the client to construct VirtualChunkContainer objects without extra
|
|
1238
|
+
API calls to fetch the full bucket config.
|
|
1235
1239
|
"""
|
|
1236
1240
|
|
|
1237
1241
|
# TODO: consider a dedicated server-side refresh endpoint to avoid exposing cross-org metadata to the client?
|
|
@@ -1243,6 +1247,55 @@ class VirtualChunkCredentials(BaseModel):
|
|
|
1243
1247
|
# where subsync writes placeholder refs like vcc://__al_source/CHUNK_ID)
|
|
1244
1248
|
# If None, assume the VCC already exists
|
|
1245
1249
|
runtime_vcc_name: str | None = None
|
|
1250
|
+
# Store config fields for constructing VCCs without extra get_bucket_config() calls.
|
|
1251
|
+
# Populated from the bucket's extra_config via from_bucket().
|
|
1252
|
+
region: str | None = None
|
|
1253
|
+
endpoint_url: str | None = None
|
|
1254
|
+
allow_http: bool = False
|
|
1255
|
+
force_path_style: bool = False
|
|
1256
|
+
|
|
1257
|
+
@classmethod
|
|
1258
|
+
def from_bucket(
|
|
1259
|
+
cls,
|
|
1260
|
+
org: OrgName,
|
|
1261
|
+
bucket: BucketModel,
|
|
1262
|
+
credentials: TempCredentials | None,
|
|
1263
|
+
) -> VirtualChunkCredentials:
|
|
1264
|
+
"""Build VirtualChunkCredentials from a bucket model, extracting store config fields."""
|
|
1265
|
+
extra = bucket.extra_config
|
|
1266
|
+
use_ssl = extra.get("use_ssl", True)
|
|
1267
|
+
region = extra.get("region_name")
|
|
1268
|
+
endpoint_url = extra.get("endpoint_url")
|
|
1269
|
+
return cls(
|
|
1270
|
+
credentials=credentials,
|
|
1271
|
+
org=org,
|
|
1272
|
+
bucket_nickname=bucket.nickname,
|
|
1273
|
+
platform=bucket.platform,
|
|
1274
|
+
region=str(region) if region is not None else None,
|
|
1275
|
+
endpoint_url=str(endpoint_url) if endpoint_url is not None else None,
|
|
1276
|
+
allow_http=not use_ssl,
|
|
1277
|
+
force_path_style=bool(extra.get("force_path_style", False if use_ssl else True)),
|
|
1278
|
+
)
|
|
1279
|
+
|
|
1280
|
+
def to_vcc_store_config(self, user_id: UUID | None = None) -> icechunk.AnyObjectStoreConfig:
|
|
1281
|
+
"""Create an Icechunk ObjectStoreConfig from the embedded store config fields."""
|
|
1282
|
+
if self.platform in ("s3", "s3-compatible", "minio"):
|
|
1283
|
+
options = icechunk.S3Options(
|
|
1284
|
+
region=self.region,
|
|
1285
|
+
endpoint_url=self.endpoint_url,
|
|
1286
|
+
allow_http=self.allow_http,
|
|
1287
|
+
force_path_style=self.force_path_style,
|
|
1288
|
+
)
|
|
1289
|
+
return icechunk.ObjectStoreConfig.S3(options)
|
|
1290
|
+
elif self.platform == "gs":
|
|
1291
|
+
from arraylake import __version__ as arraylake_version
|
|
1292
|
+
|
|
1293
|
+
config = {"user_agent": f"arraylake/{arraylake_version} (uuid={str(user_id)})" if user_id else f"arraylake/{arraylake_version}"}
|
|
1294
|
+
return icechunk.ObjectStoreConfig.Gcs(config)
|
|
1295
|
+
elif self.platform == "azure":
|
|
1296
|
+
return icechunk.ObjectStoreConfig.Azure({})
|
|
1297
|
+
else:
|
|
1298
|
+
raise ValueError(f"Unsupported platform for VCC store config: {self.platform}")
|
|
1246
1299
|
|
|
1247
1300
|
|
|
1248
1301
|
class OpenRepoResponse(BaseModel):
|
|
@@ -37,10 +37,10 @@ dependencies = [
|
|
|
37
37
|
"httpx >= 0.23, < 0.29",
|
|
38
38
|
"icechunk >= 2.0.1, < 3.0.0",
|
|
39
39
|
"numpy >= 1.23, < 3.0",
|
|
40
|
-
"packaging
|
|
40
|
+
"packaging>=23.0,<27.0",
|
|
41
41
|
"pydantic[email] >= 2.9, < 3.0",
|
|
42
42
|
"python-dateutil >= 2.8, < 3.0",
|
|
43
|
-
"rich
|
|
43
|
+
"rich>=12.6,<16.0",
|
|
44
44
|
"ruamel-yaml >= 0.17, < 1.0",
|
|
45
45
|
"structlog >= 24.1, < 26.0",
|
|
46
46
|
"typer >= 0.12.0, < 1.0",
|
|
@@ -59,25 +59,24 @@ maximal = [
|
|
|
59
59
|
|
|
60
60
|
[dependency-groups]
|
|
61
61
|
dev = [
|
|
62
|
-
"cloudpickle
|
|
62
|
+
"cloudpickle>=3.0,<3.2",
|
|
63
63
|
"hatch>=1.14.2",
|
|
64
64
|
"hypothesis>=6.88.1",
|
|
65
65
|
"icechunk >= 2.0.1, < 3.0.0",
|
|
66
66
|
"mypy~=1.11",
|
|
67
|
-
"pytest-asyncio
|
|
67
|
+
"pytest-asyncio>=0.21.1,<1.4.0",
|
|
68
68
|
"pytest-click~=1.1.0",
|
|
69
|
-
"pytest-cov
|
|
69
|
+
"pytest-cov>=4.0,<7.2",
|
|
70
70
|
"pytest-incremental~=0.6.0",
|
|
71
|
-
"pytest-
|
|
72
|
-
"pytest-
|
|
73
|
-
"pytest-
|
|
74
|
-
"pytest-
|
|
75
|
-
"pytest
|
|
76
|
-
"pytest~=7.3",
|
|
71
|
+
"pytest-reportlog>=0.4,<1.1",
|
|
72
|
+
"pytest-retry>=1.6.2,<1.8.0",
|
|
73
|
+
"pytest-timeout>=2.1,<2.5",
|
|
74
|
+
"pytest-xdist>=3.5,<3.9",
|
|
75
|
+
"pytest>=7.3,<10.0",
|
|
77
76
|
"pyyaml~=6.0.1",
|
|
78
|
-
"respx
|
|
79
|
-
"types-cachetools
|
|
80
|
-
"types-python-dateutil
|
|
77
|
+
"respx>=0.22,<0.24",
|
|
78
|
+
"types-cachetools>=5.3,<7.0",
|
|
79
|
+
"types-python-dateutil>=2.8.19,<2.10.0",
|
|
81
80
|
"types-pyyaml~=6.0.12",
|
|
82
81
|
"types-urllib3~=1.26.25",
|
|
83
82
|
]
|
|
@@ -18,6 +18,7 @@ from arraylake.api_utils import (
|
|
|
18
18
|
UserAuth,
|
|
19
19
|
_emitted_deprecation_warnings,
|
|
20
20
|
_get_proxy_from_env,
|
|
21
|
+
_get_session_id,
|
|
21
22
|
calc_backoff,
|
|
22
23
|
handle_response,
|
|
23
24
|
retry_on_exception,
|
|
@@ -81,6 +82,24 @@ async def test_http_request_headers(respx_mock, token, test_token_file) -> None:
|
|
|
81
82
|
assert route.calls.last.request.headers["special"] == "header"
|
|
82
83
|
|
|
83
84
|
|
|
85
|
+
def test_session_id_stable_within_process() -> None:
|
|
86
|
+
"""Two clients in the same process share one session id (the point of the feature)."""
|
|
87
|
+
c1 = ArraylakeHttpClient("https://foo.com")
|
|
88
|
+
c2 = ArraylakeHttpClient("https://foo.com")
|
|
89
|
+
sid = _get_session_id()
|
|
90
|
+
assert c1._default_headers["arraylake-session-id"] == sid
|
|
91
|
+
assert c2._default_headers["arraylake-session-id"] == sid
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def test_session_id_survives_pickling() -> None:
|
|
95
|
+
"""Session id rides along when the client is pickled → one dask cluster, one session."""
|
|
96
|
+
client = ArraylakeHttpClient("https://foo.com")
|
|
97
|
+
sid = client._default_headers["arraylake-session-id"]
|
|
98
|
+
assert sid
|
|
99
|
+
restored = pickle.loads(pickle.dumps(client))
|
|
100
|
+
assert restored._default_headers["arraylake-session-id"] == sid
|
|
101
|
+
|
|
102
|
+
|
|
84
103
|
def test_calc_backoff() -> None:
|
|
85
104
|
assert calc_backoff(0, backoff_factor=0.5, jitter_ratio=0.1, max_backoff_wait=10) == 0
|
|
86
105
|
assert calc_backoff(1, backoff_factor=0.5, jitter_ratio=0.0, max_backoff_wait=10) == 0.5
|