arraylake 1.0.1__tar.gz → 1.0.3a1__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.3a1}/.gitignore +1 -0
  2. {arraylake-1.0.1 → arraylake-1.0.3a1}/PKG-INFO +3 -1
  3. arraylake-1.0.3a1/arraylake/_credential_cache.py +86 -0
  4. {arraylake-1.0.1 → arraylake-1.0.3a1}/arraylake/_version.py +2 -2
  5. {arraylake-1.0.1 → arraylake-1.0.3a1}/arraylake/client.py +330 -59
  6. {arraylake-1.0.1 → arraylake-1.0.3a1}/arraylake/metastore/http_metastore.py +21 -10
  7. {arraylake-1.0.1 → arraylake-1.0.3a1}/arraylake/types.py +109 -1
  8. {arraylake-1.0.1 → arraylake-1.0.3a1}/pyproject.toml +7 -0
  9. {arraylake-1.0.1 → arraylake-1.0.3a1}/tests/test_client.py +123 -22
  10. arraylake-1.0.3a1/tests/test_credential_cache.py +237 -0
  11. {arraylake-1.0.1 → arraylake-1.0.3a1}/uv.lock +256 -348
  12. {arraylake-1.0.1 → arraylake-1.0.3a1}/README.md +0 -0
  13. {arraylake-1.0.1 → arraylake-1.0.3a1}/arraylake/__init__.py +0 -0
  14. {arraylake-1.0.1 → arraylake-1.0.3a1}/arraylake/__main__.py +0 -0
  15. {arraylake-1.0.1 → arraylake-1.0.3a1}/arraylake/api_utils.py +0 -0
  16. {arraylake-1.0.1 → arraylake-1.0.3a1}/arraylake/asyn.py +0 -0
  17. {arraylake-1.0.1 → arraylake-1.0.3a1}/arraylake/cli/__init__.py +0 -0
  18. {arraylake-1.0.1 → arraylake-1.0.3a1}/arraylake/cli/auth.py +0 -0
  19. {arraylake-1.0.1 → arraylake-1.0.3a1}/arraylake/cli/compute.py +0 -0
  20. {arraylake-1.0.1 → arraylake-1.0.3a1}/arraylake/cli/config.py +0 -0
  21. {arraylake-1.0.1 → arraylake-1.0.3a1}/arraylake/cli/main.py +0 -0
  22. {arraylake-1.0.1 → arraylake-1.0.3a1}/arraylake/cli/repo.py +0 -0
  23. {arraylake-1.0.1 → arraylake-1.0.3a1}/arraylake/cli/utils.py +0 -0
  24. {arraylake-1.0.1 → arraylake-1.0.3a1}/arraylake/compute/doctor.py +0 -0
  25. {arraylake-1.0.1 → arraylake-1.0.3a1}/arraylake/compute/http_client.py +0 -0
  26. {arraylake-1.0.1 → arraylake-1.0.3a1}/arraylake/compute/services.py +0 -0
  27. {arraylake-1.0.1 → arraylake-1.0.3a1}/arraylake/compute/types.py +0 -0
  28. {arraylake-1.0.1 → arraylake-1.0.3a1}/arraylake/config.py +0 -0
  29. {arraylake-1.0.1 → arraylake-1.0.3a1}/arraylake/config.yaml +0 -0
  30. {arraylake-1.0.1 → arraylake-1.0.3a1}/arraylake/credentials.py +0 -0
  31. {arraylake-1.0.1 → arraylake-1.0.3a1}/arraylake/diagnostics.py +0 -0
  32. {arraylake-1.0.1 → arraylake-1.0.3a1}/arraylake/display/__init__.py +0 -0
  33. {arraylake-1.0.1 → arraylake-1.0.3a1}/arraylake/display/repo.py +0 -0
  34. {arraylake-1.0.1 → arraylake-1.0.3a1}/arraylake/display/repolist.py +0 -0
  35. {arraylake-1.0.1 → arraylake-1.0.3a1}/arraylake/exceptions.py +0 -0
  36. {arraylake-1.0.1 → arraylake-1.0.3a1}/arraylake/log_util.py +0 -0
  37. {arraylake-1.0.1 → arraylake-1.0.3a1}/arraylake/metastore/__init__.py +0 -0
  38. {arraylake-1.0.1 → arraylake-1.0.3a1}/arraylake/metastore/abc.py +0 -0
  39. {arraylake-1.0.1 → arraylake-1.0.3a1}/arraylake/py.typed +0 -0
  40. {arraylake-1.0.1 → arraylake-1.0.3a1}/arraylake/repos/__init__.py +0 -0
  41. {arraylake-1.0.1 → arraylake-1.0.3a1}/arraylake/repos/icechunk/__init__.py +0 -0
  42. {arraylake-1.0.1 → arraylake-1.0.3a1}/arraylake/repos/icechunk/storage.py +0 -0
  43. {arraylake-1.0.1 → arraylake-1.0.3a1}/arraylake/repos/icechunk/types.py +0 -0
  44. {arraylake-1.0.1 → arraylake-1.0.3a1}/arraylake/repos/icechunk/virtual.py +0 -0
  45. {arraylake-1.0.1 → arraylake-1.0.3a1}/arraylake/token.py +0 -0
  46. {arraylake-1.0.1 → arraylake-1.0.3a1}/arraylake/tuning.py +0 -0
  47. {arraylake-1.0.1 → arraylake-1.0.3a1}/tests/__init__.py +0 -0
  48. {arraylake-1.0.1 → arraylake-1.0.3a1}/tests/compute/test_client.py +0 -0
  49. {arraylake-1.0.1 → arraylake-1.0.3a1}/tests/compute/test_doctor.py +0 -0
  50. {arraylake-1.0.1 → arraylake-1.0.3a1}/tests/config.yaml +0 -0
  51. {arraylake-1.0.1 → arraylake-1.0.3a1}/tests/conftest.py +0 -0
  52. {arraylake-1.0.1 → arraylake-1.0.3a1}/tests/icechunk/__init__.py +0 -0
  53. {arraylake-1.0.1 → arraylake-1.0.3a1}/tests/icechunk/conftest.py +0 -0
  54. {arraylake-1.0.1 → arraylake-1.0.3a1}/tests/icechunk/test_storage.py +0 -0
  55. {arraylake-1.0.1 → arraylake-1.0.3a1}/tests/icechunk/test_virtual.py +0 -0
  56. {arraylake-1.0.1 → arraylake-1.0.3a1}/tests/test_api_utils.py +0 -0
  57. {arraylake-1.0.1 → arraylake-1.0.3a1}/tests/test_asyn.py +0 -0
  58. {arraylake-1.0.1 → arraylake-1.0.3a1}/tests/test_cli.py +0 -0
  59. {arraylake-1.0.1 → arraylake-1.0.3a1}/tests/test_config.py +0 -0
  60. {arraylake-1.0.1 → arraylake-1.0.3a1}/tests/test_credentials.py +0 -0
  61. {arraylake-1.0.1 → arraylake-1.0.3a1}/tests/test_diagnostics.py +0 -0
  62. {arraylake-1.0.1 → arraylake-1.0.3a1}/tests/test_performance.py +0 -0
  63. {arraylake-1.0.1 → arraylake-1.0.3a1}/tests/test_subscriptions.py +0 -0
  64. {arraylake-1.0.1 → arraylake-1.0.3a1}/tests/test_token_handler.py +0 -0
  65. {arraylake-1.0.1 → arraylake-1.0.3a1}/tests/test_types.py +0 -0
  66. {arraylake-1.0.1 → arraylake-1.0.3a1}/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.3a1
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,86 @@
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
+
15
+ from __future__ import annotations
16
+
17
+ import asyncio
18
+ from collections.abc import Awaitable, Callable
19
+ from dataclasses import dataclass, field
20
+ from datetime import UTC, datetime, timedelta
21
+ from typing import Literal
22
+
23
+ from arraylake.asyn import get_background_loop
24
+ from arraylake.log_util import get_logger
25
+ from arraylake.types import OrgName, Platform, TempCredentials
26
+
27
+ logger = get_logger(__name__)
28
+
29
+
30
+ CLOCK_SKEW_SAFETY = timedelta(seconds=30)
31
+
32
+
33
+ @dataclass(eq=True, frozen=True)
34
+ class CredentialCacheKey:
35
+ api_url: str
36
+ auth_key: int
37
+ scope: Literal["repo", "bucket"]
38
+ org: OrgName
39
+ identifier: str
40
+ platform: Platform
41
+ access: Literal["read", "write"] = "read"
42
+
43
+
44
+ @dataclass
45
+ class _Entry:
46
+ lock: asyncio.Lock = field(default_factory=asyncio.Lock)
47
+ creds: TempCredentials | None = None
48
+ expires_at: datetime | None = None
49
+
50
+
51
+ _CACHE: dict[CredentialCacheKey, _Entry] = {}
52
+
53
+
54
+ async def get_or_refresh(
55
+ key: CredentialCacheKey,
56
+ fetcher: Callable[[], Awaitable[TempCredentials]],
57
+ *,
58
+ use_cache: bool = True,
59
+ ) -> TempCredentials:
60
+ assert asyncio.get_running_loop() is get_background_loop(), "credential cache must be used from the arraylake background loop"
61
+ if not use_cache:
62
+ return await fetcher()
63
+ entry = _CACHE.setdefault(key, _Entry())
64
+ async with entry.lock:
65
+ now = datetime.now(UTC)
66
+ if entry.creds is not None and entry.expires_at is not None and entry.expires_at - CLOCK_SKEW_SAFETY > now:
67
+ return entry.creds
68
+ creds = await fetcher()
69
+ exp = creds.expiration
70
+ if exp is not None and exp.tzinfo is None:
71
+ exp = exp.replace(tzinfo=UTC)
72
+ if exp is not None and exp - CLOCK_SKEW_SAFETY <= now:
73
+ logger.warning(
74
+ "delegated credentials returned with lifetime <= clock-skew safety; server contract violation",
75
+ remaining_seconds=(exp - now).total_seconds(),
76
+ api_url=key.api_url,
77
+ org=key.org,
78
+ identifier=key.identifier,
79
+ )
80
+ entry.creds = creds
81
+ entry.expires_at = exp
82
+ return creds
83
+
84
+
85
+ def clear() -> None:
86
+ _CACHE.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.3a1'
22
+ __version_tuple__ = version_tuple = (1, 0, 3, 'a1')
23
23
 
24
24
  __commit_id__ = commit_id = None