diff --git a/CHANGELOG.md b/CHANGELOG.md index 677a51f..e81e2c7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- `tilebox-workflows`: Restore `GoogleStorageCache` support for Google SDK bucket objects, preserving their + configured client, credentials, and request settings. Bucket-name strings continue to use obstore. + +## [0.63.0] - 2026-10-03 + ### Added - Support Python 3.14 across all packages. @@ -568,7 +575,8 @@ the first client that does not cache data (since it's already on the local file - Released under the [MIT](https://opensource.org/license/mit) license. - Released packages: `tilebox-datasets`, `tilebox-workflows`, `tilebox-storage`, `tilebox-grpc` -[Unreleased]: https://github.com/tilebox/tilebox-python/compare/v0.61.0...HEAD +[Unreleased]: https://github.com/tilebox/tilebox-python/compare/v0.63.0...HEAD +[0.63.0]: https://github.com/tilebox/tilebox-python/compare/v0.62.0...v0.63.0 [0.61.0]: https://github.com/tilebox/tilebox-python/compare/v0.60.0...v0.61.0 [0.60.0]: https://github.com/tilebox/tilebox-python/compare/v0.59.0...v0.60.0 [0.59.0]: https://github.com/tilebox/tilebox-python/compare/v0.58.0...v0.59.0 diff --git a/tilebox-workflows/pyproject.toml b/tilebox-workflows/pyproject.toml index 19d436a..baabefa 100644 --- a/tilebox-workflows/pyproject.toml +++ b/tilebox-workflows/pyproject.toml @@ -50,6 +50,8 @@ geospatial = [ [dependency-groups] dev = [ "boto3-stubs[s3]>=1.40.2", + "google-cloud-storage>=3.2.0", + "grpcio-status>=1.78.0", "hypothesis>=6.112.1", "ipywidgets>=8.1.7", "odc-geo>=0.5", diff --git a/tilebox-workflows/tests/test_cache.py b/tilebox-workflows/tests/test_cache.py index 80ee621..958b615 100644 --- a/tilebox-workflows/tests/test_cache.py +++ b/tilebox-workflows/tests/test_cache.py @@ -1,5 +1,6 @@ from datetime import timedelta from pathlib import Path +from typing import assert_type from unittest.mock import MagicMock, patch import boto3 @@ -170,11 +171,15 @@ def test_google_storage_credentials_and_groups() -> None: ): cache = GoogleStorageCache("cache-bucket") first = cache.group("folder") + assert_type(cache.bucket, str) + assert_type(first.bucket, str) + assert_type(first, GoogleStorageCache[str]) first["one"] = b"first" cache.group("folder2")["two"] = b"second" assert list(first) == ["one"] assert first["one"] == b"first" - assert isinstance(first, ObstoreCache) + assert isinstance(first, GoogleStorageCache) + assert first.bucket == "cache-bucket" assert first.store is store assert store.get("jobs/folder/one").bytes() == b"first" default.assert_called_once_with(scopes=["https://www.googleapis.com/auth/devstorage.read_write"]) diff --git a/tilebox-workflows/tests/test_google_cache_compatibility.py b/tilebox-workflows/tests/test_google_cache_compatibility.py new file mode 100644 index 0000000..2c95132 --- /dev/null +++ b/tilebox-workflows/tests/test_google_cache_compatibility.py @@ -0,0 +1,96 @@ +from collections.abc import Iterator +from typing import assert_type +from unittest.mock import MagicMock, patch +from urllib.parse import parse_qs, urlsplit + +import pytest +from google.api_core.exceptions import Forbidden, NotFound, ServiceUnavailable +from google.auth.credentials import AnonymousCredentials +from google.cloud.storage import Blob, Bucket, Client +from google.oauth2.credentials import Credentials +from requests import Response + +from tilebox.workflows.cache import GoogleStorageCache + + +@pytest.fixture(params=[False, True], ids=["authenticated", "anonymous"]) +def google_bucket(request: pytest.FixtureRequest) -> Iterator[Bucket]: + credentials = AnonymousCredentials() if request.param else Credentials(token="configured-token") # noqa: S106 + client = Client( + project="configured-project", + credentials=credentials, + _http=MagicMock(), + client_options={"api_endpoint": "https://custom.example.test"}, + ) + with ( + patch("google.auth.default", side_effect=AssertionError("Bucket credentials must not be replaced by ADC")), + patch("obstore.store.GCSStore", side_effect=AssertionError("Bucket configuration must stay with its client")), + ): + yield client.bucket("configured-bucket", user_project="billing-project") + + +def test_google_bucket_read_preserves_client(google_bucket: Bucket) -> None: + # Bucket reads and nested groups retain credentials, custom transport, endpoint, and requester-pays billing. + credentials = google_bucket.client._credentials + transport = google_bucket.client._http + response = Response() + response.status_code = 200 + response._content = b"configured client content" + response._content_consumed = True + response.raw = MagicMock(headers={}) + response.headers["content-length"] = str(len(response.content)) + transport.request.return_value = response + root = GoogleStorageCache(google_bucket) + cache = root.group("folder").group("nested") + assert_type(root.bucket, Bucket) + assert_type(cache.bucket, Bucket) + assert_type(cache, GoogleStorageCache[Bucket]) + assert isinstance(cache, GoogleStorageCache) + assert cache.bucket is root.bucket is google_bucket + assert cache["file.txt"] == b"configured client content" + assert google_bucket.client._credentials is credentials + # Newer Google clients may also fetch bucket metadata in a background thread. + downloads = [call for call in transport.request.call_args_list if call.args and "/download/" in call.args[1]] + assert len(downloads) == 1 + method, url = downloads[0].args + assert method == "GET" + parsed = urlsplit(url) + assert parsed.netloc == "custom.example.test" + assert parsed.path.endswith("/configured-bucket/o/jobs%2Ffolder%2Fnested%2Ffile.txt") + assert parse_qs(parsed.query)["userProject"] == ["billing-project"] + + +@pytest.mark.parametrize("prefix", ["jobs", ""]) +def test_google_bucket_operations(google_bucket: Bucket, prefix: str) -> None: + # Bucket caches preserve writes, membership, deletion, and delimiter-based listing for each group. + cache = GoogleStorageCache(google_bucket, prefix).group("folder") + path = f"{prefix}/folder" if prefix else "folder" + with ( + patch.object(Blob, "upload_from_file", autospec=True) as upload, + patch.object(Blob, "exists", autospec=True, return_value=True) as exists, + patch.object(Blob, "delete", autospec=True) as delete, + patch.object(google_bucket, "list_blobs", return_value=[Blob(f"{path}/one", google_bucket)]) as listing, + ): + cache["one"] = b"saved value" + blob, stream = upload.call_args.args + assert blob.bucket is google_bucket + assert blob.name == f"{path}/one" + assert stream.getvalue() == b"saved value" + assert "one" in cache + assert exists.call_args.args[0].name == f"{path}/one" + assert list(cache) == ["one"] + listing.assert_called_once_with(prefix=f"{path}/", delimiter="/") + del cache["one"] + assert delete.call_args.args[0].name == f"{path}/one" + + +@pytest.mark.parametrize("error_type", [NotFound, Forbidden, ServiceUnavailable]) +def test_google_bucket_read_errors(google_bucket: Bucket, error_type: type[Exception]) -> None: + # Only missing objects become KeyError; configured-client permission and backend errors propagate unchanged. + error = error_type("read failed") + with patch.object(Blob, "download_as_bytes", side_effect=error): + expected = KeyError if error_type is NotFound else error_type + with pytest.raises(expected) as raised: + GoogleStorageCache(google_bucket).group("nested")["file"] + if error_type is not NotFound: + assert raised.value is error diff --git a/tilebox-workflows/tilebox/workflows/cache.py b/tilebox-workflows/tilebox/workflows/cache.py index ffc32b8..f15f37a 100644 --- a/tilebox-workflows/tilebox/workflows/cache.py +++ b/tilebox-workflows/tilebox/workflows/cache.py @@ -1,10 +1,12 @@ import contextlib from abc import ABC, abstractmethod -from collections.abc import Iterator +from collections.abc import Iterable, Iterator +from copy import copy +from io import BytesIO from pathlib import Path from pathlib import PurePosixPath as ObjectPath from threading import RLock -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, Generic, Protocol, Self, TypeVar if TYPE_CHECKING: from obstore.store import ObjectStore @@ -257,14 +259,37 @@ def group(self, key: str) -> "LocalFileSystemCache": return LocalFileSystemCache(self.root / key) -class GoogleStorageCache(ObstoreCache): - def __init__(self, bucket: str, prefix: str | ObjectPath = "jobs") -> None: +class _GoogleBlob(Protocol): + @property + def name(self) -> str: ... + def exists(self) -> bool: ... + def upload_from_file(self, stream: BytesIO, /) -> None: ... + def download_as_bytes(self) -> bytes: ... + def delete(self) -> None: ... + + +class _GoogleBucket(Protocol): + def blob(self, name: str, /) -> _GoogleBlob: ... + def list_blobs(self, *, prefix: str, delimiter: str) -> Iterable[_GoogleBlob]: ... + + +_BucketT = TypeVar("_BucketT", bound=str | _GoogleBucket) + + +class GoogleStorageCache(ObstoreCache, Generic[_BucketT]): + def __init__(self, bucket: _BucketT, prefix: str | ObjectPath = "jobs") -> None: """A cache implementation that stores data in Google Cloud Storage. Args: - bucket: The Google Cloud Storage bucket name. Credentials come from Google Auth. + bucket: A bucket name or Google SDK Bucket. Names use obstore and Google Auth; + Bucket objects keep their existing client and its configuration. prefix: A path prefix to append to all objects stored in the cache. Defaults to "jobs". """ + self.bucket = bucket + if not isinstance(bucket, str): + self.prefix = ObjectPath(prefix) + return + import google.auth # noqa: PLC0415 from obstore.auth.google import GoogleCredentialProvider # noqa: PLC0415 from obstore.store import GCSStore # noqa: PLC0415 @@ -273,6 +298,53 @@ def __init__(self, bucket: str, prefix: str | ObjectPath = "jobs") -> None: store = GCSStore(bucket, credential_provider=GoogleCredentialProvider(credentials=credentials)) super().__init__(store, prefix) + def __contains__(self, key: str) -> bool: + if isinstance(self.bucket, str): + return super().__contains__(key) + return self.bucket.blob(str(self.prefix / key)).exists() + + def __setitem__(self, key: str, value: bytes) -> None: + if isinstance(self.bucket, str): + super().__setitem__(key, value) + else: + self.bucket.blob(str(self.prefix / key)).upload_from_file(BytesIO(value)) + + def __getitem__(self, key: str) -> bytes: + if isinstance(self.bucket, str): + return super().__getitem__(key) + + from google.api_core.exceptions import NotFound # noqa: PLC0415 + + try: + return self.bucket.blob(str(self.prefix / key)).download_as_bytes() + except NotFound: + raise KeyError(f"{key} is not cached!") from None + + def __delitem__(self, key: str) -> None: + if isinstance(self.bucket, str): + super().__delitem__(key) + return + + from google.api_core.exceptions import NotFound # noqa: PLC0415 + + try: + self.bucket.blob(str(self.prefix / key)).delete() + except NotFound: + raise KeyError(f"{key} is not cached!") from None + + def __iter__(self) -> Iterator[str]: + if isinstance(self.bucket, str): + yield from super().__iter__() + return + prefix = "" if self.prefix == ObjectPath(".") else str(self.prefix) + "/" + for blob in self.bucket.list_blobs(prefix=prefix, delimiter="/"): + yield str(ObjectPath(blob.name).relative_to(self.prefix)) + + def group(self, key: str) -> Self: + group = copy(self) + group.prefix = self.prefix / key + return group + class AmazonS3Cache(ObstoreCache): def __init__(self, bucket: str, prefix: str | ObjectPath = "jobs", *, region: str | None = None) -> None: diff --git a/uv.lock b/uv.lock index ade3ce2..cf383e4 100644 --- a/uv.lock +++ b/uv.lock @@ -780,6 +780,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/ea/53f2148663b321f21b5a606bd5f191517cf40b7072c0497d3c92c4a13b1e/executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017", size = 28317, upload-time = "2025-09-01T09:48:08.5Z" }, ] +[[package]] +name = "google-api-core" +version = "2.40.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "googleapis-common-protos" }, + { name = "opentelemetry-api" }, + { name = "proto-plus" }, + { name = "protobuf" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/f7/0fb8c3618c783ad49da1aaf97f93a4bb9e4ed522135516b44d129a7f85bb/google_api_core-2.40.0.tar.gz", hash = "sha256:ebee7d1b138b5362beecec260e6e8988ac97346562c7382ccb6f0ad8435c599f", size = 258617, upload-time = "2026-09-29T19:25:54.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/44/e4/35f849ef989a801fa4f32ada19cef898fc32913c6b93bc2a6a4f95d2df9d/google_api_core-2.40.0-py3-none-any.whl", hash = "sha256:4b9e0a80024c269ae173136d5439f4ed284651d6e5e2773ba5c694f481f9c0f4", size = 214233, upload-time = "2026-09-29T19:25:29.006Z" }, +] + [[package]] name = "google-auth" version = "2.58.0" @@ -798,6 +815,81 @@ requests = [ { name = "requests" }, ] +[[package]] +name = "google-cloud-core" +version = "2.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "google-auth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/71/d6081acadf55d4233271c39860f0d140ef61fbdb4bceb2075e9c2905d947/google_cloud_core-2.8.0.tar.gz", hash = "sha256:365f8e4518ae81c8101b8dea5fc1c32a960badedb8b511f19db2843cbbd285d2", size = 36605, upload-time = "2026-09-29T19:25:59.275Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/ed/1b09640a565e5d34d4e517463f65a67a11f9dcdeb6b6e8220f2d06aaf489/google_cloud_core-2.8.0-py3-none-any.whl", hash = "sha256:e235b0952f7ffe7b9c71a4cf96b506d9cfb557e22557c412f0df9b7068b5d007", size = 31048, upload-time = "2026-09-29T19:25:35.361Z" }, +] + +[[package]] +name = "google-cloud-storage" +version = "3.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "google-auth" }, + { name = "google-cloud-core" }, + { name = "google-crc32c" }, + { name = "google-resumable-media" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ae/54/d487cd2bcf2c94befce79baa677c54fa863f88c221e5481a4066ab08c33e/google_cloud_storage-3.16.0.tar.gz", hash = "sha256:3f75cf8ad345d68ef33f0c69db8a8b3f34f160b1524e75d73890602b7b8a4d6e", size = 17351062, upload-time = "2026-10-01T18:18:20.765Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/bf/1a0f5f7fc1d4af8b95fc1da72d3cfe84ac709ef4dcabfd2182ef9cfe3a7a/google_cloud_storage-3.16.0-py3-none-any.whl", hash = "sha256:59e54ff1f7f8a7253c01f593f79d253728c122a8901b83f68fe7464a4131539f", size = 343457, upload-time = "2026-10-01T18:12:33.141Z" }, +] + +[[package]] +name = "google-crc32c" +version = "1.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fa/25/9cb0c1c31c45b893eb8f11ae70b3f4309432d59b5acaebca5dbe791729a4/google_crc32c-1.9.0.tar.gz", hash = "sha256:7b8c84c3d159ab6817fe3f74e6e6cef099c3f95dcec3abc0d8afb1404642efbe", size = 14857, upload-time = "2026-09-24T21:39:32.067Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0e/55/a2f07f15e624f0de79359b1a6c1deb59ec5061bd3b38744b3b2849400662/google_crc32c-1.9.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:457d0d9a4718fd52b1494eac5c200ad25beeadbdc91843d550a003910838589f", size = 31958, upload-time = "2026-09-24T21:19:00.994Z" }, + { url = "https://files.pythonhosted.org/packages/f8/b3/923743597b774bbcf12a7c3e00e48d745e15fd616ad7489a40a63fff8f2f/google_crc32c-1.9.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:ccfe40021fd6afe23361175cf7551e3cef5fd34dc1ebe319f14993a83579e0eb", size = 31805, upload-time = "2026-09-24T21:22:25.019Z" }, + { url = "https://files.pythonhosted.org/packages/df/a6/4d0352fe889663e0d81cea7fc664ec9158727384de4a44ab10e9967a7682/google_crc32c-1.9.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fbef61a3794e011c65fb4396a196cf123a7f474fe5a443db8e5dd7d751b9e6d4", size = 38119, upload-time = "2026-09-24T21:38:06.634Z" }, + { url = "https://files.pythonhosted.org/packages/aa/e3/26685384e4b66ff0928d9566ef6110a7df76029175a1842329d7e3515f10/google_crc32c-1.9.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:86764b99e7a607830d93cb5b75e0ec3ff6cb06d3c274624418473cee701900d4", size = 36677, upload-time = "2026-09-24T21:38:08.082Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ce/4e90102e84880e97d3cf935f2672ecd29191bdeacf57f01740f92debda00/google_crc32c-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:43a2dc26f9be213fbe0b4fc4a1088c5d45cbfcb3247420ccc820f0fc3edeea86", size = 35096, upload-time = "2026-09-24T21:39:28.201Z" }, + { url = "https://files.pythonhosted.org/packages/e4/5d/0730e1b3a14d054d1466f2fec88dadf978509c749a3d96d8b069cc56d38a/google_crc32c-1.9.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:53fdafef58e230d0c946ab5f8446d123d9f548230a73b29c8b41c9546f268bc1", size = 31961, upload-time = "2026-09-24T21:19:01.724Z" }, + { url = "https://files.pythonhosted.org/packages/dd/32/d085abaf2fd907121975b92245bb3480fb8be40c37d03f9d6c41857f84c3/google_crc32c-1.9.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:8b91f41645b15a720357183fa5716682ada441873e3c462c15f9714be36f146b", size = 31804, upload-time = "2026-09-24T21:22:25.81Z" }, + { url = "https://files.pythonhosted.org/packages/94/78/dd1935432337e5da7af391a6fc9f161c1c8e9b9002a402b9190135fe1b59/google_crc32c-1.9.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:16865b477d7941712cb0e0aad8ad4815e984fb5fc16d3fdaef7d986e26e53c95", size = 37775, upload-time = "2026-09-24T21:38:09.249Z" }, + { url = "https://files.pythonhosted.org/packages/9e/43/9db03635bb10188d93dcbab9baa2a8670a0da4e868b4370cdbd98d65fed8/google_crc32c-1.9.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3abb18297d9ef0ab120531838be0e6d68c9fa876570e11c229c48f2edac23ce7", size = 36347, upload-time = "2026-09-24T21:38:10.141Z" }, + { url = "https://files.pythonhosted.org/packages/cf/eb/94dee516c846bd9382c3f566d8f8e5fb9e90599e45afeb697f9fc2533528/google_crc32c-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:fb63a8d7fa2e95dcff1ca16af2f4d88b526fa5ff72d1696285884ac2d49b6963", size = 35097, upload-time = "2026-09-24T21:39:28.934Z" }, + { url = "https://files.pythonhosted.org/packages/3f/34/cb484e8b6174f130f8c6dc79c733a9dd8869b410ad6511fb6104c46b973a/google_crc32c-1.9.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:f1dc17d987ddcc5eba12a7ce48f0eb93141dea236b170c1101151396edf2f0cf", size = 31960, upload-time = "2026-09-24T21:19:02.454Z" }, + { url = "https://files.pythonhosted.org/packages/af/25/3e8e567bd48448e225ea27318ccf2b94e05124e7b8b97b13eaec9e127199/google_crc32c-1.9.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:f894a2877650b56201d26a012a257b76d54a68834dc3913a93830ca8a047b075", size = 31803, upload-time = "2026-09-24T21:22:27.008Z" }, + { url = "https://files.pythonhosted.org/packages/f0/18/bee0dd59ae622482dc6463636c79e4bde7c954d061c859c9256362c9931a/google_crc32c-1.9.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4488f1553a9ab7e86cdedc833374a7e904031803b995dc0bd0be48c271fa6556", size = 37776, upload-time = "2026-09-24T21:38:11.056Z" }, + { url = "https://files.pythonhosted.org/packages/fd/b6/e76e80fed5f2558273c7839e622f98095c9b36c719c7147e38e3c055cb70/google_crc32c-1.9.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0568b17ed90ac596f29400d99e243fd0cc6276766183def888d1bf8d1dc13827", size = 36350, upload-time = "2026-09-24T21:38:12.138Z" }, + { url = "https://files.pythonhosted.org/packages/87/34/165542bfa99dfef91a76471cc48cce74b8ff4e295722896087ab2b8e8611/google_crc32c-1.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:8583ec21d56b565d68ab2963cc7e21b3b271247c29b04286068255ef65f221bd", size = 35090, upload-time = "2026-09-24T21:39:29.764Z" }, + { url = "https://files.pythonhosted.org/packages/8f/eb/43ea41f4061a1cad87b2b6559c98e960e45bf551fe66f83d833b98aaf0c9/google_crc32c-1.9.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:6a3b2c8a343c570ed8100a7627c20badfd92c6caa2067093a86be45af27f5b1b", size = 31963, upload-time = "2026-09-24T21:19:03.208Z" }, + { url = "https://files.pythonhosted.org/packages/45/d2/a968c0c29ccd2b0c980ff4f9e3f7035cee28c23a1c57541825cc8221858c/google_crc32c-1.9.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:13179f7e3282617923e957b8e54b8f9c3968030f48640a9f47fd7c5c38c4a215", size = 31805, upload-time = "2026-09-24T21:22:27.917Z" }, + { url = "https://files.pythonhosted.org/packages/03/73/388e493d6c3e252e37165d22efe5a1361f872a24425391b999822861b23a/google_crc32c-1.9.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:265233aff33d835f5b909584fe36ab29647b598c271b661a300001099109e53e", size = 37925, upload-time = "2026-09-24T21:38:13.32Z" }, + { url = "https://files.pythonhosted.org/packages/98/36/190d32caa363ef25d685f422ed1bbf93ff1140fb22fd4d90f24cec209977/google_crc32c-1.9.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dee799544cae42a42b17a88e38b59cf2c271051dc001da2117a8ff240ffa0548", size = 36495, upload-time = "2026-09-24T21:38:14.211Z" }, + { url = "https://files.pythonhosted.org/packages/d3/fd/81cefea6adae7bd92abb23d4567d199f6485a20ec0a305ca5fa04c52b9c5/google_crc32c-1.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:af73200fa9791ccd380f3598235dba8d82b8af0905df045b3dc60b59836e8ddd", size = 35535, upload-time = "2026-09-24T21:39:30.52Z" }, + { url = "https://files.pythonhosted.org/packages/c5/18/19d4f17f3f33f8fdffcb3e1e69219d6f7ec2c359c160867b04dac1d0a64d/google_crc32c-1.9.0-cp315-cp315-macosx_12_0_arm64.whl", hash = "sha256:e6e8be8a94436079cb5340f6d495d9d7ba30124d8b952703994c739c7c06e236", size = 31977, upload-time = "2026-09-24T21:19:03.976Z" }, + { url = "https://files.pythonhosted.org/packages/81/b4/8010372c4b46f2ee2352dfdb630c397570cd85522a315df024ad2f9459aa/google_crc32c-1.9.0-cp315-cp315-macosx_12_0_x86_64.whl", hash = "sha256:f2b64641bca27497b986b9d87883014035aa904cb4fa333407c6752b3afee9ba", size = 31809, upload-time = "2026-09-24T21:22:29.1Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f8/7e33845d6b90ce1cf37cfabf25cb859277c7d3533ef1b6b1e1ca58581549/google_crc32c-1.9.0-cp315-cp315-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f97c3806dcea41c29c04965347b0e12481561b75e0045dc7a4f69d75dec5d9b1", size = 38359, upload-time = "2026-09-24T21:38:14.983Z" }, + { url = "https://files.pythonhosted.org/packages/36/ff/556b2423f449a7515af6b8222a4d7833cbe09ff3e8d2f0b80471f5f6d02e/google_crc32c-1.9.0-cp315-cp315-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0abe7e202c25909869c35672ab0f2fe748a7acf276eb78577332a7c38999740f", size = 36858, upload-time = "2026-09-24T21:38:15.799Z" }, + { url = "https://files.pythonhosted.org/packages/40/71/4733f1b7c921d04a2bb9b9916cf66498bf7ad0860a06289413830da83192/google_crc32c-1.9.0-cp315-cp315-win_amd64.whl", hash = "sha256:5695c8b9327e040b2aba12c6659b0acb5995314ef0af0192da66e662e011103b", size = 35534, upload-time = "2026-09-24T21:39:31.337Z" }, +] + +[[package]] +name = "google-resumable-media" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-crc32c" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cf/64/df6a482d5aa39d7f7be186d892377d605cae6c88525fd851d456e8bbe9c9/google_resumable_media-2.11.0.tar.gz", hash = "sha256:febd83686752799661b4de575f0b993c5c25c349a5362556fc4d7be164056a37", size = 2164914, upload-time = "2026-09-29T19:26:13.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5e/2e/4f0a152f2e576e496f31ba1c3c62ed174a8878d06008916a7edc58b1bb28/google_resumable_media-2.11.0-py3-none-any.whl", hash = "sha256:f43d15e6a7f818f762eaead0f369c551f8275a4179c9d6225d0d259f49b87b5d", size = 81515, upload-time = "2026-09-29T19:25:47.31Z" }, +] + [[package]] name = "googleapis-common-protos" version = "1.75.3" @@ -871,6 +963,20 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7f/b9/69d8a709df225bc2e06e028e9465166b174c24b3da07cc72d9a5ddc63194/grpcio-1.84.0-cp315-cp315-win_amd64.whl", hash = "sha256:4119efa6519871719ad81f33bc95ab87857dcb1c5801f30a6e592f2c41164169", size = 5394757, upload-time = "2026-09-14T06:59:30.118Z" }, ] +[[package]] +name = "grpcio-status" +version = "1.84.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/45/f80309cdb6a7dbf8f65e2082dd2ddc9797ba7180516a73c54d966ba632c4/grpcio_status-1.84.0.tar.gz", hash = "sha256:5caf28ba7184b81f618b5f7f094859fd2541bf429d2189bbbcd715c9c2cdcee2", size = 14015, upload-time = "2026-09-14T07:10:29.402Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/c4/3a77e4273e866b1b0c412afd80882e95941a37170032b5109d847c501124/grpcio_status-1.84.0-py3-none-any.whl", hash = "sha256:0c182ca0d6e60acbfd0e14499cf39a155e4827a1c3fd9f7638e49af15a74c30a", size = 14639, upload-time = "2026-09-14T07:10:15.175Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -1919,6 +2025,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/6f/84908cad2d6aa5144abcf7b42709fe4fdb459bc640ec7ac5786e7693dabc/prompt_toolkit-3.0.53-py3-none-any.whl", hash = "sha256:01c0891d7f9237d5e339f7d3e42cdae80b7534abb1c7c0e3352efba6231492f2", size = 392288, upload-time = "2026-07-26T20:56:12.512Z" }, ] +[[package]] +name = "proto-plus" +version = "1.29.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/70/783e33ffbb4466cc154a94f79b869b92a451e2bd45605054e68ff68b7af6/proto_plus-1.29.0.tar.gz", hash = "sha256:cfb4e62ad7e13dd18f346cabbda00cab39930d36a05791fd81ddb074d6ee884f", size = 58592, upload-time = "2026-09-29T19:26:15.963Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/05/a3ef5b1161498e7b5a2a60288a9cf41b9149e94555423a43ec536737ac1a/proto_plus-1.29.0-py3-none-any.whl", hash = "sha256:8acd070469a7aaf43f440b022ef9757c8cac1a9f866e933f59ae98669ddc6c8b", size = 50786, upload-time = "2026-09-29T19:25:50.409Z" }, +] + [[package]] name = "protobuf" version = "7.36.2" @@ -2879,6 +2997,8 @@ notebook = [ [package.dev-dependencies] dev = [ { name = "boto3-stubs", extra = ["s3"] }, + { name = "google-cloud-storage" }, + { name = "grpcio-status" }, { name = "hypothesis" }, { name = "ipywidgets" }, { name = "moto" }, @@ -2915,6 +3035,8 @@ provides-extras = ["geospatial", "notebook"] [package.metadata.requires-dev] dev = [ { name = "boto3-stubs", extras = ["s3"], specifier = ">=1.40.2" }, + { name = "google-cloud-storage", specifier = ">=3.2.0" }, + { name = "grpcio-status", specifier = ">=1.78.0" }, { name = "hypothesis", specifier = ">=6.112.1" }, { name = "ipywidgets", specifier = ">=8.1.7" }, { name = "moto", specifier = ">=5" },