Skip to content

Commit 2b049d7

Browse files
authored
feat: Add list_async method to align with obspec (#462)
* chore: Test assignability to obspec protocols * Add list_async alias on store mixin * remove types-boto3-s3 * restore types-boto3-sts * fix boto3 type check
1 parent a70edda commit 2b049d7

5 files changed

Lines changed: 1605 additions & 1321 deletions

File tree

obstore/python/obstore/auth/boto3.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,8 @@ def __call__(self) -> S3Credential:
105105
"""Fetch credentials."""
106106
expires_at = datetime.now(timezone.utc) + self.ttl
107107
frozen_credentials = self.credentials.get_frozen_credentials()
108+
assert frozen_credentials.access_key is not None, "Access key is None"
109+
assert frozen_credentials.secret_key is not None, "Secret key is None"
108110
return {
109111
"access_key_id": frozen_credentials.access_key,
110112
"secret_access_key": frozen_credentials.secret_key,

obstore/python/obstore/store.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,6 +325,61 @@ def list(
325325
return_arrow=return_arrow,
326326
)
327327

328+
@overload
329+
def list_async(
330+
self,
331+
prefix: str | None = None,
332+
*,
333+
offset: str | None = None,
334+
chunk_size: int = 50,
335+
return_arrow: Literal[True],
336+
) -> ListStream[RecordBatch]: ...
337+
@overload
338+
def list_async(
339+
self,
340+
prefix: str | None = None,
341+
*,
342+
offset: str | None = None,
343+
chunk_size: int = 50,
344+
return_arrow: Literal[False] = False,
345+
) -> ListStream[Sequence[ObjectMeta]]: ...
346+
def list_async(
347+
self,
348+
prefix: str | None = None,
349+
*,
350+
offset: str | None = None,
351+
chunk_size: int = 50,
352+
return_arrow: bool = False,
353+
) -> ListStream[RecordBatch] | ListStream[Sequence[ObjectMeta]]:
354+
"""List all the objects with the given prefix.
355+
356+
Refer to the documentation for [list][obstore.list].
357+
358+
!!! note
359+
360+
This is an alias for `list`, provided to match the `ListAsync` protocol in
361+
obspec. There is no difference in functionality between this and the `list`
362+
method.
363+
"""
364+
# Splitting these fixes the typing issue with the `return_arrow` parameter, by
365+
# converting from a bool to a Literal[True] or Literal[False]
366+
if return_arrow:
367+
return obs.list( # type: ignore[call-overload]
368+
self, # type: ignore[arg-type]
369+
prefix,
370+
offset=offset,
371+
chunk_size=chunk_size,
372+
return_arrow=return_arrow,
373+
)
374+
375+
return obs.list( # type: ignore[call-overload]
376+
self, # type: ignore[arg-type]
377+
prefix,
378+
offset=offset,
379+
chunk_size=chunk_size,
380+
return_arrow=return_arrow,
381+
)
382+
328383
@overload
329384
def list_with_delimiter(
330385
self,

pyproject.toml

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ dev-dependencies = [
1212
"aiohttp>=3.11.13",
1313
"arro3-core>=0.4.2",
1414
"azure-identity>=1.21.0",
15-
"boto3>=1.35.38",
15+
"boto3>=1.38.21",
1616
"fastapi>=0.115.12", # used in example but added here for pyright CI
1717
"fsspec>=2024.10.0",
1818
"google-auth>=2.38.0",
@@ -29,7 +29,7 @@ dev-dependencies = [
2929
"mkdocstrings>=0.27.0",
3030
"moto[s3,server]>=5.1.1",
3131
"mypy>=1.15.0",
32-
"obspec>=0.1.0b4",
32+
"obspec>=0.1.0b5",
3333
"pip>=24.2",
3434
"polars>=1.30.0",
3535
"pyarrow>=17.0.0",
@@ -40,8 +40,8 @@ dev-dependencies = [
4040
"pytest>=8.3.3",
4141
"python-dotenv>=1.0.1",
4242
"ruff>=0.11.0",
43-
"tqdm>=4.67.1", # used in example but added here for pyright CI
44-
"types-boto3[sts]>=1.36.23",
43+
"tqdm>=4.67.1",
44+
"types-boto3[s3,sts]>=1.36.23",
4545
]
4646
constraint-dependencies = [
4747
# ensure lockfile grabs wheels for pyproj for each Python version

tests/obspec/test-store.yml

Lines changed: 52 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,10 @@
1-
- case: accepts_get
1+
- case: assignable_to_sync_store
2+
parametrized:
3+
- store: AzureStore
4+
- store: GCSStore
5+
- store: LocalStore
6+
- store: MemoryStore
7+
- store: S3Store
28
main: |
39
from typing import Protocol
410
@@ -16,8 +22,7 @@
1622
)
1723
from typing_extensions import assert_type
1824
19-
from obstore.store import AzureStore, GCSStore, LocalStore, MemoryStore, S3Store
20-
25+
from obstore.store import {{ store }}
2126
2227
class SyncStore(
2328
Copy,
@@ -33,26 +38,54 @@
3338
Protocol,
3439
): ...
3540
41+
def accepts_sync_store(store: SyncStore) -> None:
42+
assert_type(store, SyncStore)
3643
37-
def memory_store_implements_sync_store(store: MemoryStore) -> None:
38-
accepts_sync_store(store)
39-
40-
41-
def azure_store_implements_sync_store(store: AzureStore) -> None:
42-
accepts_sync_store(store)
43-
44-
45-
def gcs_store_implements_sync_store(store: GCSStore) -> None:
44+
def assignable_to_sync_store(store: {{ store }}) -> None:
4645
accepts_sync_store(store)
4746
47+
- case: assignable_to_async_store
48+
parametrized:
49+
- store: AzureStore
50+
- store: GCSStore
51+
- store: LocalStore
52+
- store: MemoryStore
53+
- store: S3Store
54+
main: |
55+
from typing import Protocol
4856
49-
def local_store_implements_sync_store(store: LocalStore) -> None:
50-
accepts_sync_store(store)
51-
57+
from obspec import (
58+
CopyAsync,
59+
DeleteAsync,
60+
GetAsync,
61+
GetRangeAsync,
62+
GetRangesAsync,
63+
HeadAsync,
64+
ListAsync,
65+
ListWithDelimiterAsync,
66+
PutAsync,
67+
RenameAsync,
68+
)
69+
from typing_extensions import assert_type
5270
53-
def s3_store_implements_sync_store(store: S3Store) -> None:
54-
accepts_sync_store(store)
71+
from obstore.store import {{ store }}
72+
73+
class AsyncStore(
74+
CopyAsync,
75+
DeleteAsync,
76+
GetAsync,
77+
GetRangeAsync,
78+
GetRangesAsync,
79+
HeadAsync,
80+
ListAsync,
81+
ListWithDelimiterAsync,
82+
PutAsync,
83+
RenameAsync,
84+
Protocol,
85+
): ...
5586
87+
def accepts_async_store(store: AsyncStore) -> None:
88+
assert_type(store, AsyncStore)
5689
57-
def accepts_sync_store(store: SyncStore) -> None:
58-
assert_type(store, SyncStore)
90+
def assignable_to_async_store(store: {{ store }}) -> None:
91+
accepts_async_store(store)

0 commit comments

Comments
 (0)