arraylake 1.0.1__tar.gz → 1.0.3__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.1 → arraylake-1.0.3}/.gitignore +1 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/PKG-INFO +3 -1
- arraylake-1.0.3/arraylake/_credential_cache.py +118 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/_version.py +2 -2
- {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/client.py +330 -59
- {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/metastore/http_metastore.py +21 -10
- {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/types.py +114 -1
- {arraylake-1.0.1 → arraylake-1.0.3}/pyproject.toml +7 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/tests/test_client.py +123 -22
- arraylake-1.0.3/tests/test_credential_cache.py +254 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/uv.lock +256 -348
- {arraylake-1.0.1 → arraylake-1.0.3}/README.md +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/__init__.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/__main__.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/api_utils.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/asyn.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/cli/__init__.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/cli/auth.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/cli/compute.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/cli/config.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/cli/main.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/cli/repo.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/cli/utils.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/compute/doctor.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/compute/http_client.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/compute/services.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/compute/types.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/config.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/config.yaml +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/credentials.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/diagnostics.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/display/__init__.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/display/repo.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/display/repolist.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/exceptions.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/log_util.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/metastore/__init__.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/metastore/abc.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/py.typed +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/repos/__init__.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/repos/icechunk/__init__.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/repos/icechunk/storage.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/repos/icechunk/types.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/repos/icechunk/virtual.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/token.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/tuning.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/tests/__init__.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/tests/compute/test_client.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/tests/compute/test_doctor.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/tests/config.yaml +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/tests/conftest.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/tests/icechunk/__init__.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/tests/icechunk/conftest.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/tests/icechunk/test_storage.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/tests/icechunk/test_virtual.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/tests/test_api_utils.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/tests/test_asyn.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/tests/test_cli.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/tests/test_config.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/tests/test_credentials.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/tests/test_diagnostics.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/tests/test_performance.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/tests/test_subscriptions.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/tests/test_token_handler.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/tests/test_types.py +0 -0
- {arraylake-1.0.1 → arraylake-1.0.3}/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.3
|
|
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
|
|
@@ -27,6 +27,8 @@ Requires-Dist: zarr<4.0,>=3.1.0
|
|
|
27
27
|
Provides-Extra: maximal
|
|
28
28
|
Requires-Dist: cf-xarray>=0.10.4; extra == 'maximal'
|
|
29
29
|
Requires-Dist: xarray[accel,etc,io,parallel]>=2024.10.0; extra == 'maximal'
|
|
30
|
+
Provides-Extra: obstore
|
|
31
|
+
Requires-Dist: obstore>=0.9.2; extra == 'obstore'
|
|
30
32
|
Provides-Extra: xarray
|
|
31
33
|
Requires-Dist: cf-xarray>=0.10.4; extra == 'xarray'
|
|
32
34
|
Requires-Dist: xarray>=2024.10.0; extra == 'xarray'
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""Process-wide cache for delegated bucket credentials.
|
|
2
|
+
|
|
3
|
+
When users pickle an icechunk Session and ship it to many workers, every
|
|
4
|
+
unpickled task can independently call the arraylake server for fresh
|
|
5
|
+
credentials when the embedded ones expire. This module gives the client a
|
|
6
|
+
single per-process cache so all consumers in a worker process share one
|
|
7
|
+
in-flight refresh.
|
|
8
|
+
|
|
9
|
+
Validity follows ``Cache-Control: max-age`` semantics: cached creds are
|
|
10
|
+
served while ``expires_at - CLOCK_SKEW_SAFETY > now``. The 30s safety only
|
|
11
|
+
protects against worker/server NTP-level clock drift; it is not a
|
|
12
|
+
pre-refresh window.
|
|
13
|
+
|
|
14
|
+
The cache is loop-agnostic: callers may live on any asyncio event loop in
|
|
15
|
+
the process. In-flight refreshes are coalesced via the singleflight
|
|
16
|
+
pattern using a single ``concurrent.futures.Future`` per key as the shared
|
|
17
|
+
result channel; each waiter bridges into its own loop via
|
|
18
|
+
``asyncio.wrap_future``. The race for who runs the fetcher is resolved by
|
|
19
|
+
``dict.setdefault``, which is atomic under the CPython GIL.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
import asyncio
|
|
25
|
+
import concurrent.futures
|
|
26
|
+
from collections.abc import Awaitable, Callable
|
|
27
|
+
from dataclasses import dataclass
|
|
28
|
+
from datetime import UTC, datetime, timedelta
|
|
29
|
+
from typing import Literal
|
|
30
|
+
|
|
31
|
+
from arraylake.log_util import get_logger
|
|
32
|
+
from arraylake.types import OrgName, Platform, TempCredentials
|
|
33
|
+
|
|
34
|
+
logger = get_logger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
CLOCK_SKEW_SAFETY = timedelta(seconds=30)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
@dataclass(eq=True, frozen=True)
|
|
41
|
+
class CredentialCacheKey:
|
|
42
|
+
api_url: str
|
|
43
|
+
auth_key: int
|
|
44
|
+
scope: Literal["repo", "bucket"]
|
|
45
|
+
org: OrgName
|
|
46
|
+
identifier: str
|
|
47
|
+
platform: Platform
|
|
48
|
+
access: Literal["read", "write"] = "read"
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass
|
|
52
|
+
class _Entry:
|
|
53
|
+
creds: TempCredentials
|
|
54
|
+
expires_at: datetime | None
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
_CACHE: dict[CredentialCacheKey, _Entry] = {}
|
|
58
|
+
_INFLIGHT: dict[CredentialCacheKey, concurrent.futures.Future[TempCredentials]] = {}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def _fresh(entry: _Entry | None, now: datetime) -> TempCredentials | None:
|
|
62
|
+
if entry is None or entry.expires_at is None:
|
|
63
|
+
return None
|
|
64
|
+
if entry.expires_at - CLOCK_SKEW_SAFETY > now:
|
|
65
|
+
return entry.creds
|
|
66
|
+
return None
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
async def get_or_refresh(
|
|
70
|
+
key: CredentialCacheKey,
|
|
71
|
+
fetcher: Callable[[], Awaitable[TempCredentials]],
|
|
72
|
+
*,
|
|
73
|
+
use_cache: bool = True,
|
|
74
|
+
) -> TempCredentials:
|
|
75
|
+
if not use_cache:
|
|
76
|
+
return await fetcher()
|
|
77
|
+
|
|
78
|
+
cached = _fresh(_CACHE.get(key), datetime.now(UTC))
|
|
79
|
+
if cached is not None:
|
|
80
|
+
return cached
|
|
81
|
+
|
|
82
|
+
my_future: concurrent.futures.Future[TempCredentials] = concurrent.futures.Future()
|
|
83
|
+
existing = _INFLIGHT.setdefault(key, my_future)
|
|
84
|
+
if existing is not my_future:
|
|
85
|
+
return await asyncio.wrap_future(existing)
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
cached = _fresh(_CACHE.get(key), datetime.now(UTC))
|
|
89
|
+
if cached is not None:
|
|
90
|
+
my_future.set_result(cached)
|
|
91
|
+
return cached
|
|
92
|
+
|
|
93
|
+
creds = await fetcher()
|
|
94
|
+
exp = creds.expiration
|
|
95
|
+
if exp is not None and exp.tzinfo is None:
|
|
96
|
+
exp = exp.replace(tzinfo=UTC)
|
|
97
|
+
now = datetime.now(UTC)
|
|
98
|
+
if exp is not None and exp - CLOCK_SKEW_SAFETY <= now:
|
|
99
|
+
logger.warning(
|
|
100
|
+
"delegated credentials returned with lifetime <= clock-skew safety; server contract violation",
|
|
101
|
+
remaining_seconds=(exp - now).total_seconds(),
|
|
102
|
+
api_url=key.api_url,
|
|
103
|
+
org=key.org,
|
|
104
|
+
identifier=key.identifier,
|
|
105
|
+
)
|
|
106
|
+
_CACHE[key] = _Entry(creds=creds, expires_at=exp)
|
|
107
|
+
my_future.set_result(creds)
|
|
108
|
+
return creds
|
|
109
|
+
except BaseException as e:
|
|
110
|
+
my_future.set_exception(e)
|
|
111
|
+
raise
|
|
112
|
+
finally:
|
|
113
|
+
_INFLIGHT.pop(key, None)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def clear() -> None:
|
|
117
|
+
_CACHE.clear()
|
|
118
|
+
_INFLIGHT.clear()
|
|
@@ -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.3'
|
|
22
|
+
__version_tuple__ = version_tuple = (1, 0, 3)
|
|
23
23
|
|
|
24
24
|
__commit_id__ = commit_id = None
|