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.
Files changed (66) hide show
  1. {arraylake-1.0.1 → arraylake-1.0.3}/.gitignore +1 -0
  2. {arraylake-1.0.1 → arraylake-1.0.3}/PKG-INFO +3 -1
  3. arraylake-1.0.3/arraylake/_credential_cache.py +118 -0
  4. {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/_version.py +2 -2
  5. {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/client.py +330 -59
  6. {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/metastore/http_metastore.py +21 -10
  7. {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/types.py +114 -1
  8. {arraylake-1.0.1 → arraylake-1.0.3}/pyproject.toml +7 -0
  9. {arraylake-1.0.1 → arraylake-1.0.3}/tests/test_client.py +123 -22
  10. arraylake-1.0.3/tests/test_credential_cache.py +254 -0
  11. {arraylake-1.0.1 → arraylake-1.0.3}/uv.lock +256 -348
  12. {arraylake-1.0.1 → arraylake-1.0.3}/README.md +0 -0
  13. {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/__init__.py +0 -0
  14. {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/__main__.py +0 -0
  15. {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/api_utils.py +0 -0
  16. {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/asyn.py +0 -0
  17. {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/cli/__init__.py +0 -0
  18. {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/cli/auth.py +0 -0
  19. {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/cli/compute.py +0 -0
  20. {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/cli/config.py +0 -0
  21. {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/cli/main.py +0 -0
  22. {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/cli/repo.py +0 -0
  23. {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/cli/utils.py +0 -0
  24. {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/compute/doctor.py +0 -0
  25. {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/compute/http_client.py +0 -0
  26. {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/compute/services.py +0 -0
  27. {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/compute/types.py +0 -0
  28. {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/config.py +0 -0
  29. {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/config.yaml +0 -0
  30. {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/credentials.py +0 -0
  31. {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/diagnostics.py +0 -0
  32. {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/display/__init__.py +0 -0
  33. {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/display/repo.py +0 -0
  34. {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/display/repolist.py +0 -0
  35. {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/exceptions.py +0 -0
  36. {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/log_util.py +0 -0
  37. {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/metastore/__init__.py +0 -0
  38. {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/metastore/abc.py +0 -0
  39. {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/py.typed +0 -0
  40. {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/repos/__init__.py +0 -0
  41. {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/repos/icechunk/__init__.py +0 -0
  42. {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/repos/icechunk/storage.py +0 -0
  43. {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/repos/icechunk/types.py +0 -0
  44. {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/repos/icechunk/virtual.py +0 -0
  45. {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/token.py +0 -0
  46. {arraylake-1.0.1 → arraylake-1.0.3}/arraylake/tuning.py +0 -0
  47. {arraylake-1.0.1 → arraylake-1.0.3}/tests/__init__.py +0 -0
  48. {arraylake-1.0.1 → arraylake-1.0.3}/tests/compute/test_client.py +0 -0
  49. {arraylake-1.0.1 → arraylake-1.0.3}/tests/compute/test_doctor.py +0 -0
  50. {arraylake-1.0.1 → arraylake-1.0.3}/tests/config.yaml +0 -0
  51. {arraylake-1.0.1 → arraylake-1.0.3}/tests/conftest.py +0 -0
  52. {arraylake-1.0.1 → arraylake-1.0.3}/tests/icechunk/__init__.py +0 -0
  53. {arraylake-1.0.1 → arraylake-1.0.3}/tests/icechunk/conftest.py +0 -0
  54. {arraylake-1.0.1 → arraylake-1.0.3}/tests/icechunk/test_storage.py +0 -0
  55. {arraylake-1.0.1 → arraylake-1.0.3}/tests/icechunk/test_virtual.py +0 -0
  56. {arraylake-1.0.1 → arraylake-1.0.3}/tests/test_api_utils.py +0 -0
  57. {arraylake-1.0.1 → arraylake-1.0.3}/tests/test_asyn.py +0 -0
  58. {arraylake-1.0.1 → arraylake-1.0.3}/tests/test_cli.py +0 -0
  59. {arraylake-1.0.1 → arraylake-1.0.3}/tests/test_config.py +0 -0
  60. {arraylake-1.0.1 → arraylake-1.0.3}/tests/test_credentials.py +0 -0
  61. {arraylake-1.0.1 → arraylake-1.0.3}/tests/test_diagnostics.py +0 -0
  62. {arraylake-1.0.1 → arraylake-1.0.3}/tests/test_performance.py +0 -0
  63. {arraylake-1.0.1 → arraylake-1.0.3}/tests/test_subscriptions.py +0 -0
  64. {arraylake-1.0.1 → arraylake-1.0.3}/tests/test_token_handler.py +0 -0
  65. {arraylake-1.0.1 → arraylake-1.0.3}/tests/test_types.py +0 -0
  66. {arraylake-1.0.1 → arraylake-1.0.3}/tests/test_virtual_chunks.py +0 -0
@@ -149,3 +149,4 @@ client/arraylake/_version.py
149
149
  !/.claude/
150
150
  /.claude/*
151
151
  !/.claude/skills/
152
+ .local/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arraylake
3
- Version: 1.0.1
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.1'
22
- __version_tuple__ = version_tuple = (1, 0, 1)
21
+ __version__ = version = '1.0.3'
22
+ __version_tuple__ = version_tuple = (1, 0, 3)
23
23
 
24
24
  __commit_id__ = commit_id = None