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.
Files changed (64) hide show
  1. {arraylake-1.0.0 → arraylake-1.0.1}/.gitignore +3 -0
  2. {arraylake-1.0.0 → arraylake-1.0.1}/PKG-INFO +3 -3
  3. {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/_version.py +2 -2
  4. {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/api_utils.py +16 -0
  5. {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/cli/auth.py +1 -8
  6. {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/client.py +9 -8
  7. {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/types.py +53 -0
  8. {arraylake-1.0.0 → arraylake-1.0.1}/pyproject.toml +13 -14
  9. {arraylake-1.0.0 → arraylake-1.0.1}/tests/test_api_utils.py +19 -0
  10. {arraylake-1.0.0 → arraylake-1.0.1}/uv.lock +427 -317
  11. {arraylake-1.0.0 → arraylake-1.0.1}/README.md +0 -0
  12. {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/__init__.py +0 -0
  13. {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/__main__.py +0 -0
  14. {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/asyn.py +0 -0
  15. {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/cli/__init__.py +0 -0
  16. {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/cli/compute.py +0 -0
  17. {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/cli/config.py +0 -0
  18. {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/cli/main.py +0 -0
  19. {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/cli/repo.py +0 -0
  20. {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/cli/utils.py +0 -0
  21. {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/compute/doctor.py +0 -0
  22. {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/compute/http_client.py +0 -0
  23. {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/compute/services.py +0 -0
  24. {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/compute/types.py +0 -0
  25. {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/config.py +0 -0
  26. {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/config.yaml +0 -0
  27. {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/credentials.py +0 -0
  28. {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/diagnostics.py +0 -0
  29. {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/display/__init__.py +0 -0
  30. {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/display/repo.py +0 -0
  31. {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/display/repolist.py +0 -0
  32. {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/exceptions.py +0 -0
  33. {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/log_util.py +0 -0
  34. {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/metastore/__init__.py +0 -0
  35. {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/metastore/abc.py +0 -0
  36. {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/metastore/http_metastore.py +0 -0
  37. {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/py.typed +0 -0
  38. {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/repos/__init__.py +0 -0
  39. {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/repos/icechunk/__init__.py +0 -0
  40. {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/repos/icechunk/storage.py +0 -0
  41. {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/repos/icechunk/types.py +0 -0
  42. {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/repos/icechunk/virtual.py +0 -0
  43. {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/token.py +0 -0
  44. {arraylake-1.0.0 → arraylake-1.0.1}/arraylake/tuning.py +0 -0
  45. {arraylake-1.0.0 → arraylake-1.0.1}/tests/__init__.py +0 -0
  46. {arraylake-1.0.0 → arraylake-1.0.1}/tests/compute/test_client.py +0 -0
  47. {arraylake-1.0.0 → arraylake-1.0.1}/tests/compute/test_doctor.py +0 -0
  48. {arraylake-1.0.0 → arraylake-1.0.1}/tests/config.yaml +0 -0
  49. {arraylake-1.0.0 → arraylake-1.0.1}/tests/conftest.py +0 -0
  50. {arraylake-1.0.0 → arraylake-1.0.1}/tests/icechunk/__init__.py +0 -0
  51. {arraylake-1.0.0 → arraylake-1.0.1}/tests/icechunk/conftest.py +0 -0
  52. {arraylake-1.0.0 → arraylake-1.0.1}/tests/icechunk/test_storage.py +0 -0
  53. {arraylake-1.0.0 → arraylake-1.0.1}/tests/icechunk/test_virtual.py +0 -0
  54. {arraylake-1.0.0 → arraylake-1.0.1}/tests/test_asyn.py +0 -0
  55. {arraylake-1.0.0 → arraylake-1.0.1}/tests/test_cli.py +0 -0
  56. {arraylake-1.0.0 → arraylake-1.0.1}/tests/test_client.py +0 -0
  57. {arraylake-1.0.0 → arraylake-1.0.1}/tests/test_config.py +0 -0
  58. {arraylake-1.0.0 → arraylake-1.0.1}/tests/test_credentials.py +0 -0
  59. {arraylake-1.0.0 → arraylake-1.0.1}/tests/test_diagnostics.py +0 -0
  60. {arraylake-1.0.0 → arraylake-1.0.1}/tests/test_performance.py +0 -0
  61. {arraylake-1.0.0 → arraylake-1.0.1}/tests/test_subscriptions.py +0 -0
  62. {arraylake-1.0.0 → arraylake-1.0.1}/tests/test_token_handler.py +0 -0
  63. {arraylake-1.0.0 → arraylake-1.0.1}/tests/test_types.py +0 -0
  64. {arraylake-1.0.0 → arraylake-1.0.1}/tests/test_virtual_chunks.py +0 -0
@@ -146,3 +146,6 @@ client/arraylake/_version.py
146
146
 
147
147
  # claude
148
148
  **/.claude/
149
+ !/.claude/
150
+ /.claude/*
151
+ !/.claude/skills/
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: arraylake
3
- Version: 1.0.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<26.0,>=23.0
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<15.0,>=12.6
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.0'
22
- __version_tuple__ = version_tuple = (1, 0, 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
- rich_console.print(
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
- # Handle runtime VCCs (e.g., filtered subscriptions) - create VCC on the fly
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
- config.set_virtual_chunk_container(container)
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 >= 23.0, < 26.0",
40
+ "packaging>=23.0,<27.0",
41
41
  "pydantic[email] >= 2.9, < 3.0",
42
42
  "python-dateutil >= 2.8, < 3.0",
43
- "rich >= 12.6, < 15.0",
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~=3.0.0",
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~=0.21.1",
67
+ "pytest-asyncio>=0.21.1,<1.4.0",
68
68
  "pytest-click~=1.1.0",
69
- "pytest-cov~=4.0.0",
69
+ "pytest-cov>=4.0,<7.2",
70
70
  "pytest-incremental~=0.6.0",
71
- "pytest-lazy-fixture~=0.6.3",
72
- "pytest-reportlog~=0.4.0",
73
- "pytest-retry~=1.6.2",
74
- "pytest-timeout~=2.1.0",
75
- "pytest-xdist~=3.5.0",
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~=0.22.0",
79
- "types-cachetools~=5.3",
80
- "types-python-dateutil~=2.8.19",
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