diff --git a/AGENTS.md b/AGENTS.md index 1f6eb246..f592223c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -249,7 +249,7 @@ type annotation issues to maintain type safety guarantees. ## Make Commands Reference | Command | Purpose | -|---------|---------| +| ------- | ------- | | `make sync` | Install all dependencies | | `make install` | Alias for `make sync` | | `make lint` | Lint Python + Markdown | diff --git a/README.md b/README.md index 8cb1eb03..44ce3e96 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ This monorepo contains two libraries: ## Why use this library? - **Multiple backends**: DynamoDB, Elasticsearch, Memcached, MongoDB, Redis, - RocksDB, Valkey, and In-memory, Disk, etc + RocksDB, Valkey, Firestore, and In-memory, Disk, etc - **TTL support**: Automatic expiration handling across all store types - **Type-safe**: Full type hints with Protocol-based interfaces - **Adapters**: Pydantic model support, raise-on-missing behavior, etc @@ -132,6 +132,8 @@ pip install py-key-value-aio[memory] pip install py-key-value-aio[disk] pip install py-key-value-aio[dynamodb] pip install py-key-value-aio[elasticsearch] +# Firestore support +pip install py-key-value-aio[firestore] # or: redis, mongodb, memcached, valkey, vault, registry, rocksdb, see below for all options ``` diff --git a/docs/adapters.md b/docs/adapters.md index a99cce15..530a38bd 100644 --- a/docs/adapters.md +++ b/docs/adapters.md @@ -7,7 +7,7 @@ provide alternative APIs tailored for specific use cases. ## Available Adapters | Adapter | Description | -|---------|-------------| +| ------- | ----------- | | [DataclassAdapter](#dataclassadapter) | Type-safe storage/retrieval of dataclass models with transparent serialization | | [PydanticAdapter](#pydanticadapter) | Type-safe storage/retrieval of Pydantic models with transparent serialization | | [RaiseOnMissingAdapter](#raiseonmissingadapter) | Optional raise-on-missing behavior for get operations | diff --git a/docs/getting-started.md b/docs/getting-started.md index 413fd6d9..1feeba07 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -30,6 +30,9 @@ pip install py-key-value-aio[elasticsearch] # MongoDB support pip install py-key-value-aio[mongodb] +# Firestore support +pip install py-key-value-aio[firestore] + # All backends pip install py-key-value-aio[all] ``` diff --git a/docs/stores.md b/docs/stores.md index ee6250a7..3dbbe305 100644 --- a/docs/stores.md +++ b/docs/stores.md @@ -30,7 +30,7 @@ long-term storage, prefer stable stores. Local stores are stored in memory or on disk, local to the application. | Store | Stability | Async | Sync | Description | -|-------|:---------:|:-----:|:----:|:------------| +| ----- | :-------: | :---: | :--: | ----------- | | Memory | N/A | ✅ | ✅ | Fast in-memory storage for development and caching | | Disk | Stable | ☑️ | ✅ | Persistent file-based storage in a single file | | Disk (Per-Collection) | Stable | ☑️ | ✅ | Persistent storage with separate files per collection | @@ -315,7 +315,7 @@ Secret stores provide secure storage for sensitive data, typically using operating system secret management facilities. | Store | Stability | Async | Sync | Description | -|-------|:---------:|:-----:|:----:|:------------| +| ----- | :-------: | :---: | :--: | ----------- | | Keyring | Stable | ✅ | ✅ | OS-level secure storage (Keychain, Credential Manager, etc.) | | Vault | Unstable | ✅ | ✅ | HashiCorp Vault integration for enterprise secrets | @@ -395,9 +395,10 @@ pip install py-key-value-aio[vault] Distributed stores provide network-based storage for multi-node applications. | Store | Stability | Async | Sync | Description | -|-------|:---------:|:-----:|:----:|:------------| +| ----- | :-------: | :---: | :--: | ----------- | | DynamoDB | Unstable | ✅ | ✖️ | AWS DynamoDB key-value storage | | Elasticsearch | Unstable | ✅ | ✅ | Full-text search with key-value capabilities | +| Firestore | Unstable | ✅ | ✅ | Google Cloud Firestore key-value storage | | Memcached | Unstable | ✅ | ✖️ | High-performance distributed memory cache | | MongoDB | Unstable | ✅ | ✅ | Document database used as key-value store | | Redis | Stable | ✅ | ✅ | Popular in-memory data structure store | @@ -467,6 +468,36 @@ pip install py-key-value-aio[valkey] --- +### FirestoreStore + +Google Cloud Firestore used as a key-value store. + +```python +from key_value.aio.stores.firestore import FirestoreStore + +store = FirestoreStore(credentials=google_credentials, database="firestore-db") +``` + +**Installation:** + +```bash +pip install py-key-value-aio[firestore] +``` + +**Use Cases:** + +- Google Cloud-native applications +- Serverless / managed infrastructure +- Existing Firestore deployments + +**Characteristics:** + +- Managed cloud database +- Document/collection model +- Stable storage format: **Unstable** + +--- + ### DynamoDBStore AWS DynamoDB integration for serverless and cloud-native applications. diff --git a/docs/wrappers.md b/docs/wrappers.md index ee595c63..0c30c132 100644 --- a/docs/wrappers.md +++ b/docs/wrappers.md @@ -7,7 +7,7 @@ protocol, so they can be used anywhere a store can be used. ## Available Wrappers | Wrapper | Description | -|---------|-------------| +| ------- | ----------- | | [CompressionWrapper](#compressionwrapper) | Compress values before storing and decompress on retrieval | | [FernetEncryptionWrapper](#fernetencryptionwrapper) | Encrypt values before storing and decrypt on retrieval | | [FallbackWrapper](#fallbackwrapper) | Fallback to a secondary store when the primary store fails | diff --git a/key-value/key-value-aio/pyproject.toml b/key-value/key-value-aio/pyproject.toml index 6d118c18..81359f1f 100644 --- a/key-value/key-value-aio/pyproject.toml +++ b/key-value/key-value-aio/pyproject.toml @@ -50,6 +50,7 @@ rocksdb = [ "rocksdict>=0.3.2 ; python_version < '3.12'" ] duckdb = ["duckdb>=1.1.1", "pytz>=2025.2"] +firestore = ["google-cloud-firestore>=2.13.0", "google-auth>=2.24.0"] wrappers-encryption = ["cryptography>=45.0.0"] [tool.pytest.ini_options] @@ -70,7 +71,7 @@ env_files = [".env"] [dependency-groups] dev = [ - "py-key-value-aio[memory,disk,filetree,redis,elasticsearch,memcached,mongodb,vault,dynamodb,rocksdb,duckdb]", + "py-key-value-aio[memory,disk,filetree,redis,elasticsearch,memcached,mongodb,vault,dynamodb,rocksdb,duckdb,firestore]", "py-key-value-aio[valkey]; platform_system != 'Windows'", "py-key-value-aio[keyring]", "py-key-value-aio[pydantic]", diff --git a/key-value/key-value-aio/src/key_value/aio/stores/firestore/__init__.py b/key-value/key-value-aio/src/key_value/aio/stores/firestore/__init__.py new file mode 100644 index 00000000..5fcc3591 --- /dev/null +++ b/key-value/key-value-aio/src/key_value/aio/stores/firestore/__init__.py @@ -0,0 +1,5 @@ +"""Firestore key-value store.""" + +from key_value.aio.stores.firestore.store import FirestoreStore + +__all__ = ["FirestoreStore"] diff --git a/key-value/key-value-aio/src/key_value/aio/stores/firestore/store.py b/key-value/key-value-aio/src/key_value/aio/stores/firestore/store.py new file mode 100644 index 00000000..f84360fb --- /dev/null +++ b/key-value/key-value-aio/src/key_value/aio/stores/firestore/store.py @@ -0,0 +1,121 @@ +from typing import overload + +from key_value.shared.utils.managed_entry import ManagedEntry +from typing_extensions import override + +from key_value.aio.stores.base import ( + BaseContextManagerStore, + BaseStore, + BasicSerializationAdapter, +) + +try: + from google.cloud import firestore + from google.oauth2.service_account import Credentials +except ImportError as e: + msg = "FirestoreStore requires py-key-value-aio[firestore]" + raise ImportError(msg) from e + + +class FirestoreStore(BaseContextManagerStore, BaseStore): + """Firestore-based key-value store. + + This store uses Firebase DB as the key-value storage. + The data is stored in collections. + """ + + _client: firestore.AsyncClient | None + + @overload + def __init__(self, client: firestore.AsyncClient, *, default_collection: str | None = None) -> None: + """Initialize the Firestore store with a client. It defers project and database from client instance. + + Args: + client: The initialized Firestore client to use. + default_collection: The default collection to use if no collection is provided. + """ + + @overload + def __init__( + self, *, credentials: Credentials, project: str | None = None, database: str | None = None, default_collection: str | None = None + ) -> None: + """Initialize the Firestore store with Google service account credentials. + + Args: + credentials: Google service account credentials from google-cloud-auth module. + project: Google project name. + database: database name, defaults to '(default)' if not provided. + default_collection: The default collection to use if no collection is provided. + """ + + def __init__( + self, + client: firestore.AsyncClient | None = None, + *, + credentials: Credentials | None = None, + project: str | None = None, + database: str | None = None, + default_collection: str | None = None, + ) -> None: + """Initialize the Firestore store with Google client or Google service account credentials. + If provided with a client, uses it, otherwise connects using credentials. + + Args: + client: The initialized Firestore client to use. Chosen by default if provided. + credentials: Google service account credentials from google-cloud-auth module. + project: Google project name. + database: database name, defaults to '(default)' if not provided. + default_collection: The default collection to use if no collection is provided. + """ + self._credentials = credentials + self._project = project + self._database = database + serialization_adapter = BasicSerializationAdapter(value_format="string") + + if client: + self._client = client + client_provided_by_user = True + else: + self._client = firestore.AsyncClient(credentials=self._credentials, project=self._project, database=self._database) + client_provided_by_user = False + super().__init__( + default_collection=default_collection, + client_provided_by_user=client_provided_by_user, + serialization_adapter=serialization_adapter, + ) + + @property + def _connected_client(self) -> firestore.AsyncClient: + if not self._client: + msg = "Client not connected" + raise ValueError(msg) + return self._client + + @override + async def _get_managed_entry(self, *, key: str, collection: str | None = None) -> ManagedEntry | None: + """Get a managed entry from Firestore.""" + collection = collection or self.default_collection + response = await self._connected_client.collection(collection).document(key).get() # pyright: ignore[reportUnknownMemberType] + doc = response.to_dict() + if doc is None: + return None + return self._serialization_adapter.load_dict(data=doc) + + @override + async def _put_managed_entry(self, *, key: str, managed_entry: ManagedEntry, collection: str | None = None) -> None: + """Store a managed entry in Firestore.""" + collection = collection or self.default_collection + item = self._serialization_adapter.dump_dict(entry=managed_entry) + await self._connected_client.collection(collection).document(key).set(item) # pyright: ignore[reportUnknownMemberType] + + @override + async def _delete_managed_entry(self, *, key: str, collection: str | None = None) -> bool: + """Delete a managed entry from Firestore.""" + collection = collection or self.default_collection + await self._connected_client.collection(collection).document(key).delete() + return True + + async def _close(self) -> None: + """Close the Firestore client.""" + if self._client and not self._client_provided_by_user: + self._client.close() diff --git a/key-value/key-value-aio/tests/stores/firestore/__init__.py b/key-value/key-value-aio/tests/stores/firestore/__init__.py new file mode 100644 index 00000000..b5f588be --- /dev/null +++ b/key-value/key-value-aio/tests/stores/firestore/__init__.py @@ -0,0 +1 @@ +"""Tests for Firestore store.""" diff --git a/key-value/key-value-aio/tests/stores/firestore/test_firestore.py b/key-value/key-value-aio/tests/stores/firestore/test_firestore.py new file mode 100644 index 00000000..e5bb247d --- /dev/null +++ b/key-value/key-value-aio/tests/stores/firestore/test_firestore.py @@ -0,0 +1,132 @@ +# pyright: reportIncompatibleMethodOverride=false + +from typing import Any + +import pytest +from typing_extensions import override + +from key_value.aio.stores.base import BaseStore + +try: + from google.cloud import firestore + + from key_value.aio.stores.firestore import FirestoreStore +except ImportError: # pragma: no cover + pytest.skip("Firestore dependencies not installed. Install with `py-key-value-aio[firestore]`.", allow_module_level=True) + +from tests.stores.base import BaseStoreTests, ContextManagerStoreTestMixin + + +class _InMemoryAsyncFirestoreDocument: + def __init__(self, storage: dict[tuple[str, str], dict[str, Any]], collection: str, key: str) -> None: + self._storage = storage + self._collection = collection + self._key = key + + async def get(self) -> "_InMemoryAsyncFirestoreDocumentSnapshot": + return _InMemoryAsyncFirestoreDocumentSnapshot(self._storage.get((self._collection, self._key))) + + async def set(self, data: dict[str, Any]) -> None: + self._storage[(self._collection, self._key)] = data + + async def delete(self) -> None: + self._storage.pop((self._collection, self._key), None) + + +class _InMemoryAsyncFirestoreDocumentSnapshot: + def __init__(self, data: dict[str, Any] | None) -> None: + self._data = data + + @property + def exists(self) -> bool: + return self._data is not None + + def to_dict(self) -> dict[str, Any] | None: + if self._data is None: + return None + return dict(self._data) + + +class _InMemoryAsyncFirestoreCollection: + def __init__(self, storage: dict[tuple[str, str], dict[str, Any]], name: str) -> None: + self._storage = storage + self._name = name + + def document(self, key: str) -> _InMemoryAsyncFirestoreDocument: + return _InMemoryAsyncFirestoreDocument(storage=self._storage, collection=self._name, key=key) + + +class InMemoryAsyncFirestoreClient(firestore.AsyncClient): + """Minimal in-memory Firestore AsyncClient replacement for tests. + + This client mimics the subset of the Firestore AsyncClient API that + FirestoreStore relies on: `collection().document().get/set/delete()`. + """ + + __slots__ = ("_storage", "closed") + + def __init__(self) -> None: + self._storage: dict[tuple[str, str], dict[str, Any]] = {} + self.closed = False + + def collection(self, name: str) -> _InMemoryAsyncFirestoreCollection: + return _InMemoryAsyncFirestoreCollection(storage=self._storage, name=name) + + def close(self) -> None: + self.closed = True + + +@pytest.mark.filterwarnings("ignore:A configured store is unstable and may change in a backwards incompatible way. Use at your own risk.") +class TestFirestoreStore(ContextManagerStoreTestMixin, BaseStoreTests): + @override + @pytest.fixture + async def store(self) -> FirestoreStore: + client = InMemoryAsyncFirestoreClient() + return FirestoreStore(client=client, default_collection="test") + + @override + @pytest.mark.skip(reason="Distributed cloud stores are unbounded") + async def test_not_unbounded(self, store: BaseStore): ... + + @override + async def test_delete(self, store: BaseStore): + # Firestore deletes are idempotent and do not fail for missing keys. + assert await store.delete(collection="test", key="test") is True + + @override + async def test_put_delete_delete(self, store: BaseStore): + # Firestore deletes are idempotent and do not fail for missing keys. + await store.put(collection="test", key="test", value={"test": "test"}) + assert await store.delete(collection="test", key="test") is True + assert await store.delete(collection="test", key="test") is True + + @override + async def test_delete_many(self, store: BaseStore): + # Firestore deletes are idempotent and do not fail for missing keys. + assert await store.delete_many(collection="test", keys=["test", "test_2"]) == 2 + + @override + async def test_put_delete_many(self, store: BaseStore): + # Firestore deletes are idempotent and do not fail for missing keys. + await store.put(collection="test", key="test", value={"test": "test"}) + assert await store.delete_many(collection="test", keys=["test", "test_2"]) == 2 + + @override + async def test_delete_many_delete_many(self, store: BaseStore): + # Firestore deletes are idempotent and do not fail for missing keys. + await store.put(collection="test", key="test", value={"test": "test"}) + assert await store.delete_many(collection="test", keys=["test", "test_2"]) == 2 + assert await store.delete_many(collection="test", keys=["test", "test_2"]) == 2 + + async def test_default_collection_used_when_collection_missing(self, store: FirestoreStore): + # When no collection is provided, the store should use the default collection + await store.put(key="test_key", value={"value": "from_default"}, collection=None) + assert await store.get(key="test_key", collection=None) == {"value": "from_default"} + + async def test_delete_returns_true_when_document_deleted(self, store: FirestoreStore): + await store.put(collection="test", key="test_key", value={"test": "value"}) + assert await store.get(collection="test", key="test_key") == {"test": "value"} + + deleted = await store.delete(collection="test", key="test_key") + assert deleted is True + assert await store.get(collection="test", key="test_key") is None diff --git a/key-value/key-value-sync/pyproject.toml b/key-value/key-value-sync/pyproject.toml index 6efdfe17..9d7aebe0 100644 --- a/key-value/key-value-sync/pyproject.toml +++ b/key-value/key-value-sync/pyproject.toml @@ -49,7 +49,30 @@ rocksdb = [ "rocksdict>=0.3.2 ; python_version < '3.12'" ] duckdb = ["duckdb>=1.1.1", "pytz>=2025.2"] +firestore = ["google-cloud-firestore>=2.13.0", "google-auth>=2.24.0"] wrappers-encryption = ["cryptography>=45.0.0"] +all = [ + "cachetools>=5.0.0", + "diskcache>=5.0.0", + "pathvalidate>=3.3.1", + "anyio>=4.4.0", + "redis>=4.3.0", + "pymongo>=4.0.0", + "valkey-glide-sync>=2.1.0", + "hvac>=2.3.0", + "types-hvac>=2.3.0", + "aiomcache>=0.8.0", + "elasticsearch>=8.0.0", + "aiohttp>=3.12", + "keyring>=25.6.0", + "pydantic>=2.11.9", + "rocksdict>=0.3.24 ; python_version >= '3.12'", + "rocksdict>=0.3.2 ; python_version < '3.12'", + "duckdb>=1.1.1", + "pytz>=2025.2", + "google-cloud-firestore>=2.13.0", + "google-auth>=2.24.0", +] [tool.pytest.ini_options] asyncio_mode = "auto" @@ -69,7 +92,7 @@ env_files = [".env"] [dependency-groups] dev = [ - "py-key-value-sync[memory,disk,filetree,redis,elasticsearch,memcached,mongodb,vault,rocksdb,duckdb]", + "py-key-value-sync[memory,disk,filetree,redis,elasticsearch,memcached,mongodb,vault,rocksdb,duckdb,firestore]", "py-key-value-sync[valkey]; platform_system != 'Windows'", "py-key-value-sync[pydantic]", "py-key-value-sync[keyring]", @@ -85,4 +108,4 @@ extends = "../../pyproject.toml" exclude = [ "src/key_value/sync/code_gen/stores/redis/store.py" -] \ No newline at end of file +] diff --git a/key-value/key-value-sync/src/key_value/sync/code_gen/stores/firestore/__init__.py b/key-value/key-value-sync/src/key_value/sync/code_gen/stores/firestore/__init__.py new file mode 100644 index 00000000..9ba1c6c0 --- /dev/null +++ b/key-value/key-value-sync/src/key_value/sync/code_gen/stores/firestore/__init__.py @@ -0,0 +1,8 @@ +# WARNING: this file is auto-generated by 'build_sync_library.py' +# from the original file '__init__.py' +# DO NOT CHANGE! Change the original file instead. +"""Firestore key-value store.""" + +from key_value.sync.code_gen.stores.firestore.store import FirestoreStore + +__all__ = ["FirestoreStore"] diff --git a/key-value/key-value-sync/src/key_value/sync/code_gen/stores/firestore/store.py b/key-value/key-value-sync/src/key_value/sync/code_gen/stores/firestore/store.py new file mode 100644 index 00000000..b98387ad --- /dev/null +++ b/key-value/key-value-sync/src/key_value/sync/code_gen/stores/firestore/store.py @@ -0,0 +1,120 @@ +# WARNING: this file is auto-generated by 'build_sync_library.py' +# from the original file 'store.py' +# DO NOT CHANGE! Change the original file instead. +from typing import overload + +from key_value.shared.utils.managed_entry import ManagedEntry +from typing_extensions import override + +from key_value.sync.code_gen.stores.base import BaseContextManagerStore, BaseStore, BasicSerializationAdapter + +try: + from google.cloud import firestore + from google.oauth2.service_account import Credentials +except ImportError as e: + msg = "FirestoreStore requires py-key-value-aio[firestore]" + raise ImportError(msg) from e + + +class FirestoreStore(BaseContextManagerStore, BaseStore): + """Firestore-based key-value store. + + This store uses Firebase DB as the key-value storage. + The data is stored in collections. + """ + + _client: firestore.Client | None + + @overload + def __init__(self, client: firestore.Client, *, default_collection: str | None = None) -> None: + """Initialize the Firestore store with a client. It defers project and database from client instance. + + Args: + client: The initialized Firestore client to use. + default_collection: The default collection to use if no collection is provided. + """ + + @overload + def __init__( + self, *, credentials: Credentials, project: str | None = None, database: str | None = None, default_collection: str | None = None + ) -> None: + """Initialize the Firestore store with Google service account credentials. + + Args: + credentials: Google service account credentials from google-cloud-auth module. + project: Google project name. + database: database name, defaults to '(default)' if not provided. + default_collection: The default collection to use if no collection is provided. + """ + + def __init__( + self, + client: firestore.Client | None = None, + *, + credentials: Credentials | None = None, + project: str | None = None, + database: str | None = None, + default_collection: str | None = None, + ) -> None: + """Initialize the Firestore store with Google client or Google service account credentials. + If provided with a client, uses it, otherwise connects using credentials. + + Args: + client: The initialized Firestore client to use. Chosen by default if provided. + credentials: Google service account credentials from google-cloud-auth module. + project: Google project name. + database: database name, defaults to '(default)' if not provided. + default_collection: The default collection to use if no collection is provided. + """ + self._credentials = credentials + self._project = project + self._database = database + serialization_adapter = BasicSerializationAdapter(value_format="string") + + if client: + self._client = client + client_provided_by_user = True + else: + self._client = firestore.Client(credentials=self._credentials, project=self._project, database=self._database) + client_provided_by_user = False + super().__init__( + default_collection=default_collection, + client_provided_by_user=client_provided_by_user, + serialization_adapter=serialization_adapter, + ) + + @property + def _connected_client(self) -> firestore.Client: + if not self._client: + msg = "Client not connected" + raise ValueError(msg) + return self._client + + @override + def _get_managed_entry(self, *, key: str, collection: str | None = None) -> ManagedEntry | None: + """Get a managed entry from Firestore.""" + collection = collection or self.default_collection + response = self._connected_client.collection(collection).document(key).get() # pyright: ignore[reportUnknownMemberType] + doc = response.to_dict() + if doc is None: + return None + return self._serialization_adapter.load_dict(data=doc) + + @override + def _put_managed_entry(self, *, key: str, managed_entry: ManagedEntry, collection: str | None = None) -> None: + """Store a managed entry in Firestore.""" + collection = collection or self.default_collection + item = self._serialization_adapter.dump_dict(entry=managed_entry) + self._connected_client.collection(collection).document(key).set(item) # pyright: ignore[reportUnknownMemberType] + + @override + def _delete_managed_entry(self, *, key: str, collection: str | None = None) -> bool: + """Delete a managed entry from Firestore.""" + collection = collection or self.default_collection + self._connected_client.collection(collection).document(key).delete() + return True + + def _close(self) -> None: + """Close the Firestore client.""" + if self._client and (not self._client_provided_by_user): + self._client.close() diff --git a/key-value/key-value-sync/src/key_value/sync/stores/firestore/__init__.py b/key-value/key-value-sync/src/key_value/sync/stores/firestore/__init__.py new file mode 100644 index 00000000..9ba1c6c0 --- /dev/null +++ b/key-value/key-value-sync/src/key_value/sync/stores/firestore/__init__.py @@ -0,0 +1,8 @@ +# WARNING: this file is auto-generated by 'build_sync_library.py' +# from the original file '__init__.py' +# DO NOT CHANGE! Change the original file instead. +"""Firestore key-value store.""" + +from key_value.sync.code_gen.stores.firestore.store import FirestoreStore + +__all__ = ["FirestoreStore"] diff --git a/key-value/key-value-sync/tests/code_gen/stores/firestore/__init__.py b/key-value/key-value-sync/tests/code_gen/stores/firestore/__init__.py new file mode 100644 index 00000000..08964bd6 --- /dev/null +++ b/key-value/key-value-sync/tests/code_gen/stores/firestore/__init__.py @@ -0,0 +1,4 @@ +# WARNING: this file is auto-generated by 'build_sync_library.py' +# from the original file '__init__.py' +# DO NOT CHANGE! Change the original file instead. +"""Tests for Firestore store.""" diff --git a/key-value/key-value-sync/tests/code_gen/stores/firestore/test_firestore.py b/key-value/key-value-sync/tests/code_gen/stores/firestore/test_firestore.py new file mode 100644 index 00000000..1d2a7e27 --- /dev/null +++ b/key-value/key-value-sync/tests/code_gen/stores/firestore/test_firestore.py @@ -0,0 +1,135 @@ +# WARNING: this file is auto-generated by 'build_sync_library.py' +# from the original file 'test_firestore.py' +# DO NOT CHANGE! Change the original file instead. +# pyright: reportIncompatibleMethodOverride=false + +from typing import Any + +import pytest +from typing_extensions import override + +from key_value.sync.code_gen.stores.base import BaseStore + +try: + from google.cloud import firestore + + from key_value.sync.code_gen.stores.firestore import FirestoreStore +except ImportError: # pragma: no cover + pytest.skip("Firestore dependencies not installed. Install with `py-key-value-aio[firestore]`.", allow_module_level=True) + +from tests.code_gen.stores.base import BaseStoreTests, ContextManagerStoreTestMixin + + +class _InMemoryAsyncFirestoreDocument: + def __init__(self, storage: dict[tuple[str, str], dict[str, Any]], collection: str, key: str) -> None: + self._storage = storage + self._collection = collection + self._key = key + + def get(self) -> "_InMemoryAsyncFirestoreDocumentSnapshot": + return _InMemoryAsyncFirestoreDocumentSnapshot(self._storage.get((self._collection, self._key))) + + def set(self, data: dict[str, Any]) -> None: + self._storage[self._collection, self._key] = data + + def delete(self) -> None: + self._storage.pop((self._collection, self._key), None) + + +class _InMemoryAsyncFirestoreDocumentSnapshot: + def __init__(self, data: dict[str, Any] | None) -> None: + self._data = data + + @property + def exists(self) -> bool: + return self._data is not None + + def to_dict(self) -> dict[str, Any] | None: + if self._data is None: + return None + return dict(self._data) + + +class _InMemoryAsyncFirestoreCollection: + def __init__(self, storage: dict[tuple[str, str], dict[str, Any]], name: str) -> None: + self._storage = storage + self._name = name + + def document(self, key: str) -> _InMemoryAsyncFirestoreDocument: + return _InMemoryAsyncFirestoreDocument(storage=self._storage, collection=self._name, key=key) + + +class InMemoryAsyncFirestoreClient(firestore.Client): + """Minimal in-memory Firestore Client replacement for tests. + + This client mimics the subset of the Firestore Client API that + FirestoreStore relies on: `collection().document().get/set/delete()`. + """ + + __slots__ = ("_storage", "closed") + + def __init__(self) -> None: + self._storage: dict[tuple[str, str], dict[str, Any]] = {} + self.closed = False + + def collection(self, name: str) -> _InMemoryAsyncFirestoreCollection: + return _InMemoryAsyncFirestoreCollection(storage=self._storage, name=name) + + def close(self) -> None: + self.closed = True + + +@pytest.mark.filterwarnings("ignore:A configured store is unstable and may change in a backwards incompatible way. Use at your own risk.") +class TestFirestoreStore(ContextManagerStoreTestMixin, BaseStoreTests): + @override + @pytest.fixture + def store(self) -> FirestoreStore: + client = InMemoryAsyncFirestoreClient() + return FirestoreStore(client=client, default_collection="test") + + @override + @pytest.mark.skip(reason="Distributed cloud stores are unbounded") + def test_not_unbounded(self, store: BaseStore): ... + + @override + def test_delete(self, store: BaseStore): + # Firestore deletes are idempotent and do not fail for missing keys. + assert store.delete(collection="test", key="test") is True + + @override + def test_put_delete_delete(self, store: BaseStore): + # Firestore deletes are idempotent and do not fail for missing keys. + store.put(collection="test", key="test", value={"test": "test"}) + assert store.delete(collection="test", key="test") is True + assert store.delete(collection="test", key="test") is True + + @override + def test_delete_many(self, store: BaseStore): + # Firestore deletes are idempotent and do not fail for missing keys. + assert store.delete_many(collection="test", keys=["test", "test_2"]) == 2 + + @override + def test_put_delete_many(self, store: BaseStore): + # Firestore deletes are idempotent and do not fail for missing keys. + store.put(collection="test", key="test", value={"test": "test"}) + assert store.delete_many(collection="test", keys=["test", "test_2"]) == 2 + + @override + def test_delete_many_delete_many(self, store: BaseStore): + # Firestore deletes are idempotent and do not fail for missing keys. + store.put(collection="test", key="test", value={"test": "test"}) + assert store.delete_many(collection="test", keys=["test", "test_2"]) == 2 + assert store.delete_many(collection="test", keys=["test", "test_2"]) == 2 + + def test_default_collection_used_when_collection_missing(self, store: FirestoreStore): + # When no collection is provided, the store should use the default collection + store.put(key="test_key", value={"value": "from_default"}, collection=None) + assert store.get(key="test_key", collection=None) == {"value": "from_default"} + + def test_delete_returns_true_when_document_deleted(self, store: FirestoreStore): + store.put(collection="test", key="test_key", value={"test": "value"}) + assert store.get(collection="test", key="test_key") == {"test": "value"} + + deleted = store.delete(collection="test", key="test_key") + assert deleted is True + assert store.get(collection="test", key="test_key") is None diff --git a/scripts/build_sync_library.py b/scripts/build_sync_library.py index a054f4ac..284f8dd9 100644 --- a/scripts/build_sync_library.py +++ b/scripts/build_sync_library.py @@ -221,6 +221,7 @@ class RenameAsyncToSync(ast.NodeTransformer): # type: ignore "__aiter__": "__iter__", "asyncio.locks": "threading", "AsyncElasticsearch": "Elasticsearch", + "AsyncClient": "Client", "AsyncDatabase": "Database", "AsyncCollection": "Collection", "AsyncMongoClient": "MongoClient", diff --git a/uv.lock b/uv.lock index a71afc2a..3becd9ba 100644 --- a/uv.lock +++ b/uv.lock @@ -1,11 +1,17 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.10" resolution-markers = [ - "python_full_version >= '3.12' and sys_platform != 'win32'", - "python_full_version >= '3.12' and sys_platform == 'win32'", - "python_full_version < '3.12' and sys_platform != 'win32'", - "python_full_version < '3.12' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform != 'win32'", + "python_full_version == '3.12.*' and sys_platform != 'win32'", + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.12.*' and sys_platform == 'win32'", + "python_full_version == '3.11.*' and sys_platform != 'win32'", + "python_full_version < '3.11' and sys_platform != 'win32'", + "python_full_version == '3.11.*' and sys_platform == 'win32'", + "python_full_version < '3.11' and sys_platform == 'win32'", ] [manifest] @@ -806,7 +812,7 @@ name = "exceptiongroup" version = "1.3.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.12'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } wheels = [ @@ -964,6 +970,83 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, ] +[[package]] +name = "google-api-core" +version = "2.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "googleapis-common-protos" }, + { name = "proto-plus" }, + { name = "protobuf" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/da/83d7043169ac2c8c7469f0e375610d78ae2160134bf1b80634c482fa079c/google_api_core-2.28.1.tar.gz", hash = "sha256:2b405df02d68e68ce0fbc138559e6036559e685159d148ae5861013dc201baf8", size = 176759, upload-time = "2025-10-28T21:34:51.529Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/d4/90197b416cb61cefd316964fd9e7bd8324bcbafabf40eef14a9f20b81974/google_api_core-2.28.1-py3-none-any.whl", hash = "sha256:4021b0f8ceb77a6fb4de6fde4502cecab45062e66ff4f2895169e0b35bc9466c", size = 173706, upload-time = "2025-10-28T21:34:50.151Z" }, +] + +[package.optional-dependencies] +grpc = [ + { name = "grpcio" }, + { name = "grpcio-status" }, +] + +[[package]] +name = "google-auth" +version = "2.43.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cachetools" }, + { name = "pyasn1-modules" }, + { name = "rsa" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ff/ef/66d14cf0e01b08d2d51ffc3c20410c4e134a1548fc246a6081eae585a4fe/google_auth-2.43.0.tar.gz", hash = "sha256:88228eee5fc21b62a1b5fe773ca15e67778cb07dc8363adcb4a8827b52d81483", size = 296359, upload-time = "2025-11-06T00:13:36.587Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/d1/385110a9ae86d91cc14c5282c61fe9f4dc41c0b9f7d423c6ad77038c4448/google_auth-2.43.0-py2.py3-none-any.whl", hash = "sha256:af628ba6fa493f75c7e9dbe9373d148ca9f4399b5ea29976519e0a3848eddd16", size = 223114, upload-time = "2025-11-06T00:13:35.209Z" }, +] + +[[package]] +name = "google-cloud-core" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "google-auth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/03/ef0bc99d0e0faf4fdbe67ac445e18cdaa74824fd93cd069e7bb6548cb52d/google_cloud_core-2.5.0.tar.gz", hash = "sha256:7c1b7ef5c92311717bd05301aa1a91ffbc565673d3b0b4163a52d8413a186963", size = 36027, upload-time = "2025-10-29T23:17:39.513Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/20/bfa472e327c8edee00f04beecc80baeddd2ab33ee0e86fd7654da49d45e9/google_cloud_core-2.5.0-py3-none-any.whl", hash = "sha256:67d977b41ae6c7211ee830c7912e41003ea8194bff15ae7d72fd6f51e57acabc", size = 29469, upload-time = "2025-10-29T23:17:38.548Z" }, +] + +[[package]] +name = "google-cloud-firestore" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core", extra = ["grpc"] }, + { name = "google-auth" }, + { name = "google-cloud-core" }, + { name = "proto-plus" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/9d/027b9bf61a44422bcdcb00a2acc59152065b1cffa1fc89da62277730973e/google_cloud_firestore-2.21.0.tar.gz", hash = "sha256:0c37faa8506297f827eefc38feb155247a6dcb9a541289631015d125f1b003f8", size = 528159, upload-time = "2025-06-03T19:28:27.195Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0a/03/94755c64a2fb85cba734ac05a4f80096b8c0acfab0508c9d52c57f571687/google_cloud_firestore-2.21.0-py3-none-any.whl", hash = "sha256:bf33ccc38a27afc60748d1f9bb7c46b078d0d39d288636bdfd967611d7b3f17f", size = 368813, upload-time = "2025-06-03T19:28:25.131Z" }, +] + +[[package]] +name = "googleapis-common-protos" +version = "1.72.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e5/7b/adfd75544c415c487b33061fe7ae526165241c1ea133f9a9125a56b39fd8/googleapis_common_protos-1.72.0.tar.gz", hash = "sha256:e55a601c1b32b52d7a3e65f43563e2aa61bcd737998ee672ac9b951cd49319f5", size = 147433, upload-time = "2025-11-06T18:29:24.087Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515, upload-time = "2025-11-06T18:29:13.14Z" }, +] + [[package]] name = "griffe" version = "1.14.0" @@ -976,6 +1059,81 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/b1/9ff6578d789a89812ff21e4e0f80ffae20a65d5dd84e7a17873fe3b365be/griffe-1.14.0-py3-none-any.whl", hash = "sha256:0e9d52832cccf0f7188cfe585ba962d2674b241c01916d780925df34873bceb0", size = 144439, upload-time = "2025-09-05T15:02:27.511Z" }, ] +[[package]] +name = "grpcio" +version = "1.76.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182, upload-time = "2025-10-21T16:23:12.106Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/17/ff4795dc9a34b6aee6ec379f1b66438a3789cd1315aac0cbab60d92f74b3/grpcio-1.76.0-cp310-cp310-linux_armv7l.whl", hash = "sha256:65a20de41e85648e00305c1bb09a3598f840422e522277641145a32d42dcefcc", size = 5840037, upload-time = "2025-10-21T16:20:25.069Z" }, + { url = "https://files.pythonhosted.org/packages/4e/ff/35f9b96e3fa2f12e1dcd58a4513a2e2294a001d64dec81677361b7040c9a/grpcio-1.76.0-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:40ad3afe81676fd9ec6d9d406eda00933f218038433980aa19d401490e46ecde", size = 11836482, upload-time = "2025-10-21T16:20:30.113Z" }, + { url = "https://files.pythonhosted.org/packages/3e/1c/8374990f9545e99462caacea5413ed783014b3b66ace49e35c533f07507b/grpcio-1.76.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:035d90bc79eaa4bed83f524331d55e35820725c9fbb00ffa1904d5550ed7ede3", size = 6407178, upload-time = "2025-10-21T16:20:32.733Z" }, + { url = "https://files.pythonhosted.org/packages/1e/77/36fd7d7c75a6c12542c90a6d647a27935a1ecaad03e0ffdb7c42db6b04d2/grpcio-1.76.0-cp310-cp310-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4215d3a102bd95e2e11b5395c78562967959824156af11fa93d18fdd18050990", size = 7075684, upload-time = "2025-10-21T16:20:35.435Z" }, + { url = "https://files.pythonhosted.org/packages/38/f7/e3cdb252492278e004722306c5a8935eae91e64ea11f0af3437a7de2e2b7/grpcio-1.76.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:49ce47231818806067aea3324d4bf13825b658ad662d3b25fada0bdad9b8a6af", size = 6611133, upload-time = "2025-10-21T16:20:37.541Z" }, + { url = "https://files.pythonhosted.org/packages/7e/20/340db7af162ccd20a0893b5f3c4a5d676af7b71105517e62279b5b61d95a/grpcio-1.76.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:8cc3309d8e08fd79089e13ed4819d0af72aa935dd8f435a195fd152796752ff2", size = 7195507, upload-time = "2025-10-21T16:20:39.643Z" }, + { url = "https://files.pythonhosted.org/packages/10/f0/b2160addc1487bd8fa4810857a27132fb4ce35c1b330c2f3ac45d697b106/grpcio-1.76.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:971fd5a1d6e62e00d945423a567e42eb1fa678ba89072832185ca836a94daaa6", size = 8160651, upload-time = "2025-10-21T16:20:42.492Z" }, + { url = "https://files.pythonhosted.org/packages/2c/2c/ac6f98aa113c6ef111b3f347854e99ebb7fb9d8f7bb3af1491d438f62af4/grpcio-1.76.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:9d9adda641db7207e800a7f089068f6f645959f2df27e870ee81d44701dd9db3", size = 7620568, upload-time = "2025-10-21T16:20:45.995Z" }, + { url = "https://files.pythonhosted.org/packages/90/84/7852f7e087285e3ac17a2703bc4129fafee52d77c6c82af97d905566857e/grpcio-1.76.0-cp310-cp310-win32.whl", hash = "sha256:063065249d9e7e0782d03d2bca50787f53bd0fb89a67de9a7b521c4a01f1989b", size = 3998879, upload-time = "2025-10-21T16:20:48.592Z" }, + { url = "https://files.pythonhosted.org/packages/10/30/d3d2adcbb6dd3ff59d6ac3df6ef830e02b437fb5c90990429fd180e52f30/grpcio-1.76.0-cp310-cp310-win_amd64.whl", hash = "sha256:a6ae758eb08088d36812dd5d9af7a9859c05b1e0f714470ea243694b49278e7b", size = 4706892, upload-time = "2025-10-21T16:20:50.697Z" }, + { url = "https://files.pythonhosted.org/packages/a0/00/8163a1beeb6971f66b4bbe6ac9457b97948beba8dd2fc8e1281dce7f79ec/grpcio-1.76.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2e1743fbd7f5fa713a1b0a8ac8ebabf0ec980b5d8809ec358d488e273b9cf02a", size = 5843567, upload-time = "2025-10-21T16:20:52.829Z" }, + { url = "https://files.pythonhosted.org/packages/10/c1/934202f5cf335e6d852530ce14ddb0fef21be612ba9ecbbcbd4d748ca32d/grpcio-1.76.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:a8c2cf1209497cf659a667d7dea88985e834c24b7c3b605e6254cbb5076d985c", size = 11848017, upload-time = "2025-10-21T16:20:56.705Z" }, + { url = "https://files.pythonhosted.org/packages/11/0b/8dec16b1863d74af6eb3543928600ec2195af49ca58b16334972f6775663/grpcio-1.76.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:08caea849a9d3c71a542827d6df9d5a69067b0a1efbea8a855633ff5d9571465", size = 6412027, upload-time = "2025-10-21T16:20:59.3Z" }, + { url = "https://files.pythonhosted.org/packages/d7/64/7b9e6e7ab910bea9d46f2c090380bab274a0b91fb0a2fe9b0cd399fffa12/grpcio-1.76.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f0e34c2079d47ae9f6188211db9e777c619a21d4faba6977774e8fa43b085e48", size = 7075913, upload-time = "2025-10-21T16:21:01.645Z" }, + { url = "https://files.pythonhosted.org/packages/68/86/093c46e9546073cefa789bd76d44c5cb2abc824ca62af0c18be590ff13ba/grpcio-1.76.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8843114c0cfce61b40ad48df65abcfc00d4dba82eae8718fab5352390848c5da", size = 6615417, upload-time = "2025-10-21T16:21:03.844Z" }, + { url = "https://files.pythonhosted.org/packages/f7/b6/5709a3a68500a9c03da6fb71740dcdd5ef245e39266461a03f31a57036d8/grpcio-1.76.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8eddfb4d203a237da6f3cc8a540dad0517d274b5a1e9e636fd8d2c79b5c1d397", size = 7199683, upload-time = "2025-10-21T16:21:06.195Z" }, + { url = "https://files.pythonhosted.org/packages/91/d3/4b1f2bf16ed52ce0b508161df3a2d186e4935379a159a834cb4a7d687429/grpcio-1.76.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:32483fe2aab2c3794101c2a159070584e5db11d0aa091b2c0ea9c4fc43d0d749", size = 8163109, upload-time = "2025-10-21T16:21:08.498Z" }, + { url = "https://files.pythonhosted.org/packages/5c/61/d9043f95f5f4cf085ac5dd6137b469d41befb04bd80280952ffa2a4c3f12/grpcio-1.76.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dcfe41187da8992c5f40aa8c5ec086fa3672834d2be57a32384c08d5a05b4c00", size = 7626676, upload-time = "2025-10-21T16:21:10.693Z" }, + { url = "https://files.pythonhosted.org/packages/36/95/fd9a5152ca02d8881e4dd419cdd790e11805979f499a2e5b96488b85cf27/grpcio-1.76.0-cp311-cp311-win32.whl", hash = "sha256:2107b0c024d1b35f4083f11245c0e23846ae64d02f40b2b226684840260ed054", size = 3997688, upload-time = "2025-10-21T16:21:12.746Z" }, + { url = "https://files.pythonhosted.org/packages/60/9c/5c359c8d4c9176cfa3c61ecd4efe5affe1f38d9bae81e81ac7186b4c9cc8/grpcio-1.76.0-cp311-cp311-win_amd64.whl", hash = "sha256:522175aba7af9113c48ec10cc471b9b9bd4f6ceb36aeb4544a8e2c80ed9d252d", size = 4709315, upload-time = "2025-10-21T16:21:15.26Z" }, + { url = "https://files.pythonhosted.org/packages/bf/05/8e29121994b8d959ffa0afd28996d452f291b48cfc0875619de0bde2c50c/grpcio-1.76.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:81fd9652b37b36f16138611c7e884eb82e0cec137c40d3ef7c3f9b3ed00f6ed8", size = 5799718, upload-time = "2025-10-21T16:21:17.939Z" }, + { url = "https://files.pythonhosted.org/packages/d9/75/11d0e66b3cdf998c996489581bdad8900db79ebd83513e45c19548f1cba4/grpcio-1.76.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:04bbe1bfe3a68bbfd4e52402ab7d4eb59d72d02647ae2042204326cf4bbad280", size = 11825627, upload-time = "2025-10-21T16:21:20.466Z" }, + { url = "https://files.pythonhosted.org/packages/28/50/2f0aa0498bc188048f5d9504dcc5c2c24f2eb1a9337cd0fa09a61a2e75f0/grpcio-1.76.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d388087771c837cdb6515539f43b9d4bf0b0f23593a24054ac16f7a960be16f4", size = 6359167, upload-time = "2025-10-21T16:21:23.122Z" }, + { url = "https://files.pythonhosted.org/packages/66/e5/bbf0bb97d29ede1d59d6588af40018cfc345b17ce979b7b45424628dc8bb/grpcio-1.76.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:9f8f757bebaaea112c00dba718fc0d3260052ce714e25804a03f93f5d1c6cc11", size = 7044267, upload-time = "2025-10-21T16:21:25.995Z" }, + { url = "https://files.pythonhosted.org/packages/f5/86/f6ec2164f743d9609691115ae8ece098c76b894ebe4f7c94a655c6b03e98/grpcio-1.76.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:980a846182ce88c4f2f7e2c22c56aefd515daeb36149d1c897f83cf57999e0b6", size = 6573963, upload-time = "2025-10-21T16:21:28.631Z" }, + { url = "https://files.pythonhosted.org/packages/60/bc/8d9d0d8505feccfdf38a766d262c71e73639c165b311c9457208b56d92ae/grpcio-1.76.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f92f88e6c033db65a5ae3d97905c8fea9c725b63e28d5a75cb73b49bda5024d8", size = 7164484, upload-time = "2025-10-21T16:21:30.837Z" }, + { url = "https://files.pythonhosted.org/packages/67/e6/5d6c2fc10b95edf6df9b8f19cf10a34263b7fd48493936fffd5085521292/grpcio-1.76.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4baf3cbe2f0be3289eb68ac8ae771156971848bb8aaff60bad42005539431980", size = 8127777, upload-time = "2025-10-21T16:21:33.577Z" }, + { url = "https://files.pythonhosted.org/packages/3f/c8/dce8ff21c86abe025efe304d9e31fdb0deaaa3b502b6a78141080f206da0/grpcio-1.76.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:615ba64c208aaceb5ec83bfdce7728b80bfeb8be97562944836a7a0a9647d882", size = 7594014, upload-time = "2025-10-21T16:21:41.882Z" }, + { url = "https://files.pythonhosted.org/packages/e0/42/ad28191ebf983a5d0ecef90bab66baa5a6b18f2bfdef9d0a63b1973d9f75/grpcio-1.76.0-cp312-cp312-win32.whl", hash = "sha256:45d59a649a82df5718fd9527ce775fd66d1af35e6d31abdcdc906a49c6822958", size = 3984750, upload-time = "2025-10-21T16:21:44.006Z" }, + { url = "https://files.pythonhosted.org/packages/9e/00/7bd478cbb851c04a48baccaa49b75abaa8e4122f7d86da797500cccdd771/grpcio-1.76.0-cp312-cp312-win_amd64.whl", hash = "sha256:c088e7a90b6017307f423efbb9d1ba97a22aa2170876223f9709e9d1de0b5347", size = 4704003, upload-time = "2025-10-21T16:21:46.244Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ed/71467ab770effc9e8cef5f2e7388beb2be26ed642d567697bb103a790c72/grpcio-1.76.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2", size = 5807716, upload-time = "2025-10-21T16:21:48.475Z" }, + { url = "https://files.pythonhosted.org/packages/2c/85/c6ed56f9817fab03fa8a111ca91469941fb514e3e3ce6d793cb8f1e1347b/grpcio-1.76.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:45e0111e73f43f735d70786557dc38141185072d7ff8dc1829d6a77ac1471468", size = 11821522, upload-time = "2025-10-21T16:21:51.142Z" }, + { url = "https://files.pythonhosted.org/packages/ac/31/2b8a235ab40c39cbc141ef647f8a6eb7b0028f023015a4842933bc0d6831/grpcio-1.76.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83d57312a58dcfe2a3a0f9d1389b299438909a02db60e2f2ea2ae2d8034909d3", size = 6362558, upload-time = "2025-10-21T16:21:54.213Z" }, + { url = "https://files.pythonhosted.org/packages/bd/64/9784eab483358e08847498ee56faf8ff6ea8e0a4592568d9f68edc97e9e9/grpcio-1.76.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3e2a27c89eb9ac3d81ec8835e12414d73536c6e620355d65102503064a4ed6eb", size = 7049990, upload-time = "2025-10-21T16:21:56.476Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/8c12319a6369434e7a184b987e8e9f3b49a114c489b8315f029e24de4837/grpcio-1.76.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae", size = 6575387, upload-time = "2025-10-21T16:21:59.051Z" }, + { url = "https://files.pythonhosted.org/packages/15/0f/f12c32b03f731f4a6242f771f63039df182c8b8e2cf8075b245b409259d4/grpcio-1.76.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a15c17af8839b6801d554263c546c69c4d7718ad4321e3166175b37eaacca77", size = 7166668, upload-time = "2025-10-21T16:22:02.049Z" }, + { url = "https://files.pythonhosted.org/packages/ff/2d/3ec9ce0c2b1d92dd59d1c3264aaec9f0f7c817d6e8ac683b97198a36ed5a/grpcio-1.76.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25a18e9810fbc7e7f03ec2516addc116a957f8cbb8cbc95ccc80faa072743d03", size = 8124928, upload-time = "2025-10-21T16:22:04.984Z" }, + { url = "https://files.pythonhosted.org/packages/1a/74/fd3317be5672f4856bcdd1a9e7b5e17554692d3db9a3b273879dc02d657d/grpcio-1.76.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:931091142fd8cc14edccc0845a79248bc155425eee9a98b2db2ea4f00a235a42", size = 7589983, upload-time = "2025-10-21T16:22:07.881Z" }, + { url = "https://files.pythonhosted.org/packages/45/bb/ca038cf420f405971f19821c8c15bcbc875505f6ffadafe9ffd77871dc4c/grpcio-1.76.0-cp313-cp313-win32.whl", hash = "sha256:5e8571632780e08526f118f74170ad8d50fb0a48c23a746bef2a6ebade3abd6f", size = 3984727, upload-time = "2025-10-21T16:22:10.032Z" }, + { url = "https://files.pythonhosted.org/packages/41/80/84087dc56437ced7cdd4b13d7875e7439a52a261e3ab4e06488ba6173b0a/grpcio-1.76.0-cp313-cp313-win_amd64.whl", hash = "sha256:f9f7bd5faab55f47231ad8dba7787866b69f5e93bc306e3915606779bbfb4ba8", size = 4702799, upload-time = "2025-10-21T16:22:12.709Z" }, + { url = "https://files.pythonhosted.org/packages/b4/46/39adac80de49d678e6e073b70204091e76631e03e94928b9ea4ecf0f6e0e/grpcio-1.76.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:ff8a59ea85a1f2191a0ffcc61298c571bc566332f82e5f5be1b83c9d8e668a62", size = 5808417, upload-time = "2025-10-21T16:22:15.02Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f5/a4531f7fb8b4e2a60b94e39d5d924469b7a6988176b3422487be61fe2998/grpcio-1.76.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:06c3d6b076e7b593905d04fdba6a0525711b3466f43b3400266f04ff735de0cd", size = 11828219, upload-time = "2025-10-21T16:22:17.954Z" }, + { url = "https://files.pythonhosted.org/packages/4b/1c/de55d868ed7a8bd6acc6b1d6ddc4aa36d07a9f31d33c912c804adb1b971b/grpcio-1.76.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd5ef5932f6475c436c4a55e4336ebbe47bd3272be04964a03d316bbf4afbcbc", size = 6367826, upload-time = "2025-10-21T16:22:20.721Z" }, + { url = "https://files.pythonhosted.org/packages/59/64/99e44c02b5adb0ad13ab3adc89cb33cb54bfa90c74770f2607eea629b86f/grpcio-1.76.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b331680e46239e090f5b3cead313cc772f6caa7d0fc8de349337563125361a4a", size = 7049550, upload-time = "2025-10-21T16:22:23.637Z" }, + { url = "https://files.pythonhosted.org/packages/43/28/40a5be3f9a86949b83e7d6a2ad6011d993cbe9b6bd27bea881f61c7788b6/grpcio-1.76.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2229ae655ec4e8999599469559e97630185fdd53ae1e8997d147b7c9b2b72cba", size = 6575564, upload-time = "2025-10-21T16:22:26.016Z" }, + { url = "https://files.pythonhosted.org/packages/4b/a9/1be18e6055b64467440208a8559afac243c66a8b904213af6f392dc2212f/grpcio-1.76.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:490fa6d203992c47c7b9e4a9d39003a0c2bcc1c9aa3c058730884bbbb0ee9f09", size = 7176236, upload-time = "2025-10-21T16:22:28.362Z" }, + { url = "https://files.pythonhosted.org/packages/0f/55/dba05d3fcc151ce6e81327541d2cc8394f442f6b350fead67401661bf041/grpcio-1.76.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:479496325ce554792dba6548fae3df31a72cef7bad71ca2e12b0e58f9b336bfc", size = 8125795, upload-time = "2025-10-21T16:22:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/4a/45/122df922d05655f63930cf42c9e3f72ba20aadb26c100ee105cad4ce4257/grpcio-1.76.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c9b93f79f48b03ada57ea24725d83a30284a012ec27eab2cf7e50a550cbbbcc", size = 7592214, upload-time = "2025-10-21T16:22:33.831Z" }, + { url = "https://files.pythonhosted.org/packages/4a/6e/0b899b7f6b66e5af39e377055fb4a6675c9ee28431df5708139df2e93233/grpcio-1.76.0-cp314-cp314-win32.whl", hash = "sha256:747fa73efa9b8b1488a95d0ba1039c8e2dca0f741612d80415b1e1c560febf4e", size = 4062961, upload-time = "2025-10-21T16:22:36.468Z" }, + { url = "https://files.pythonhosted.org/packages/19/41/0b430b01a2eb38ee887f88c1f07644a1df8e289353b78e82b37ef988fb64/grpcio-1.76.0-cp314-cp314-win_amd64.whl", hash = "sha256:922fa70ba549fce362d2e2871ab542082d66e2aaf0c19480ea453905b01f384e", size = 4834462, upload-time = "2025-10-21T16:22:39.772Z" }, +] + +[[package]] +name = "grpcio-status" +version = "1.76.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "googleapis-common-protos" }, + { name = "grpcio" }, + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3f/46/e9f19d5be65e8423f886813a2a9d0056ba94757b0c5007aa59aed1a961fa/grpcio_status-1.76.0.tar.gz", hash = "sha256:25fcbfec74c15d1a1cb5da3fab8ee9672852dc16a5a9eeb5baf7d7a9952943cd", size = 13679, upload-time = "2025-10-21T16:28:52.545Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/cc/27ba60ad5a5f2067963e6a858743500df408eb5855e98be778eaef8c9b02/grpcio_status-1.76.0-py3-none-any.whl", hash = "sha256:380568794055a8efbbd8871162df92012e0228a5f6dffaf57f2a00c534103b18", size = 14425, upload-time = "2025-10-21T16:28:40.853Z" }, +] + [[package]] name = "hvac" version = "2.3.0" @@ -1693,6 +1851,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, ] +[[package]] +name = "proto-plus" +version = "1.26.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/ac/87285f15f7cce6d4a008f33f1757fb5a13611ea8914eb58c3d0d26243468/proto_plus-1.26.1.tar.gz", hash = "sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012", size = 56142, upload-time = "2025-03-10T15:54:38.843Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/6d/280c4c2ce28b1593a19ad5239c8b826871fc6ec275c21afc8e1820108039/proto_plus-1.26.1-py3-none-any.whl", hash = "sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66", size = 50163, upload-time = "2025-03-10T15:54:37.335Z" }, +] + [[package]] name = "protobuf" version = "6.33.0" @@ -1790,6 +1960,10 @@ filetree = [ { name = "aiofile" }, { name = "anyio" }, ] +firestore = [ + { name = "google-auth" }, + { name = "google-cloud-firestore" }, +] keyring = [ { name = "keyring" }, ] @@ -1829,7 +2003,7 @@ wrappers-encryption = [ [package.dev-dependencies] dev = [ { name = "py-key-value", extra = ["dev"] }, - { name = "py-key-value-aio", extra = ["disk", "duckdb", "dynamodb", "elasticsearch", "filetree", "keyring", "memcached", "memory", "mongodb", "pydantic", "redis", "rocksdb", "vault", "wrappers-encryption"] }, + { name = "py-key-value-aio", extra = ["disk", "duckdb", "dynamodb", "elasticsearch", "filetree", "firestore", "keyring", "memcached", "memory", "mongodb", "pydantic", "redis", "rocksdb", "vault", "wrappers-encryption"] }, { name = "py-key-value-aio", extra = ["valkey"], marker = "sys_platform != 'win32'" }, ] @@ -1847,6 +2021,8 @@ requires-dist = [ { name = "diskcache", marker = "extra == 'disk'", specifier = ">=5.0.0" }, { name = "duckdb", marker = "extra == 'duckdb'", specifier = ">=1.1.1" }, { name = "elasticsearch", marker = "extra == 'elasticsearch'", specifier = ">=8.0.0" }, + { name = "google-auth", marker = "extra == 'firestore'", specifier = ">=2.24.0" }, + { name = "google-cloud-firestore", marker = "extra == 'firestore'", specifier = ">=2.13.0" }, { name = "hvac", marker = "extra == 'vault'", specifier = ">=2.3.0" }, { name = "keyring", marker = "extra == 'keyring'", specifier = ">=25.6.0" }, { name = "keyring", marker = "extra == 'keyring-linux'", specifier = ">=25.6.0" }, @@ -1862,13 +2038,13 @@ requires-dist = [ { name = "types-hvac", marker = "extra == 'vault'", specifier = ">=2.3.0" }, { name = "valkey-glide", marker = "extra == 'valkey'", specifier = ">=2.1.0" }, ] -provides-extras = ["memory", "disk", "filetree", "redis", "mongodb", "valkey", "vault", "memcached", "elasticsearch", "dynamodb", "keyring", "keyring-linux", "pydantic", "rocksdb", "duckdb", "wrappers-encryption"] +provides-extras = ["memory", "disk", "filetree", "redis", "mongodb", "valkey", "vault", "memcached", "elasticsearch", "dynamodb", "keyring", "keyring-linux", "pydantic", "rocksdb", "duckdb", "firestore", "wrappers-encryption"] [package.metadata.requires-dev] dev = [ { name = "py-key-value", extras = ["dev"], editable = "." }, { name = "py-key-value-aio", extras = ["keyring"] }, - { name = "py-key-value-aio", extras = ["memory", "disk", "filetree", "redis", "elasticsearch", "memcached", "mongodb", "vault", "dynamodb", "rocksdb", "duckdb"] }, + { name = "py-key-value-aio", extras = ["memory", "disk", "filetree", "redis", "elasticsearch", "memcached", "mongodb", "vault", "dynamodb", "rocksdb", "duckdb", "firestore"] }, { name = "py-key-value-aio", extras = ["pydantic"] }, { name = "py-key-value-aio", extras = ["valkey"], marker = "sys_platform != 'win32'" }, { name = "py-key-value-aio", extras = ["wrappers-encryption"] }, @@ -1944,6 +2120,27 @@ dependencies = [ ] [package.optional-dependencies] +all = [ + { name = "aiohttp" }, + { name = "aiomcache" }, + { name = "anyio" }, + { name = "cachetools" }, + { name = "diskcache" }, + { name = "duckdb" }, + { name = "elasticsearch" }, + { name = "google-auth" }, + { name = "google-cloud-firestore" }, + { name = "hvac" }, + { name = "keyring" }, + { name = "pathvalidate" }, + { name = "pydantic" }, + { name = "pymongo" }, + { name = "pytz" }, + { name = "redis" }, + { name = "rocksdict" }, + { name = "types-hvac" }, + { name = "valkey-glide-sync" }, +] disk = [ { name = "diskcache" }, { name = "pathvalidate" }, @@ -1959,6 +2156,10 @@ elasticsearch = [ filetree = [ { name = "anyio" }, ] +firestore = [ + { name = "google-auth" }, + { name = "google-cloud-firestore" }, +] keyring = [ { name = "keyring" }, ] @@ -1998,48 +2199,91 @@ wrappers-encryption = [ [package.dev-dependencies] dev = [ { name = "py-key-value", extra = ["dev"] }, - { name = "py-key-value-sync", extra = ["disk", "duckdb", "elasticsearch", "filetree", "keyring", "memcached", "memory", "mongodb", "pydantic", "redis", "rocksdb", "vault", "wrappers-encryption"] }, + { name = "py-key-value-sync", extra = ["disk", "duckdb", "elasticsearch", "filetree", "firestore", "keyring", "memcached", "memory", "mongodb", "pydantic", "redis", "rocksdb", "vault", "wrappers-encryption"] }, { name = "py-key-value-sync", extra = ["valkey"], marker = "sys_platform != 'win32'" }, ] [package.metadata] requires-dist = [ + { name = "aiohttp", marker = "extra == 'all'", specifier = ">=3.12" }, { name = "aiohttp", marker = "extra == 'elasticsearch'", specifier = ">=3.12" }, + { name = "aiomcache", marker = "extra == 'all'", specifier = ">=0.8.0" }, { name = "aiomcache", marker = "extra == 'memcached'", specifier = ">=0.8.0" }, + { name = "anyio", marker = "extra == 'all'", specifier = ">=4.4.0" }, { name = "anyio", marker = "extra == 'filetree'", specifier = ">=4.4.0" }, { name = "beartype", specifier = ">=0.20.0" }, + { name = "cachetools", marker = "extra == 'all'", specifier = ">=5.0.0" }, { name = "cachetools", marker = "extra == 'memory'", specifier = ">=5.0.0" }, { name = "cryptography", marker = "extra == 'wrappers-encryption'", specifier = ">=45.0.0" }, { name = "dbus-python", marker = "extra == 'keyring-linux'", specifier = ">=1.4.0" }, + { name = "diskcache", marker = "extra == 'all'", specifier = ">=5.0.0" }, { name = "diskcache", marker = "extra == 'disk'", specifier = ">=5.0.0" }, + { name = "duckdb", marker = "extra == 'all'", specifier = ">=1.1.1" }, { name = "duckdb", marker = "extra == 'duckdb'", specifier = ">=1.1.1" }, + { name = "elasticsearch", marker = "extra == 'all'", specifier = ">=8.0.0" }, { name = "elasticsearch", marker = "extra == 'elasticsearch'", specifier = ">=8.0.0" }, + { name = "google-auth", marker = "extra == 'all'", specifier = ">=2.24.0" }, + { name = "google-auth", marker = "extra == 'firestore'", specifier = ">=2.24.0" }, + { name = "google-cloud-firestore", marker = "extra == 'all'", specifier = ">=2.13.0" }, + { name = "google-cloud-firestore", marker = "extra == 'firestore'", specifier = ">=2.13.0" }, + { name = "hvac", marker = "extra == 'all'", specifier = ">=2.3.0" }, { name = "hvac", marker = "extra == 'vault'", specifier = ">=2.3.0" }, + { name = "keyring", marker = "extra == 'all'", specifier = ">=25.6.0" }, { name = "keyring", marker = "extra == 'keyring'", specifier = ">=25.6.0" }, { name = "keyring", marker = "extra == 'keyring-linux'", specifier = ">=25.6.0" }, + { name = "pathvalidate", marker = "extra == 'all'", specifier = ">=3.3.1" }, { name = "pathvalidate", marker = "extra == 'disk'", specifier = ">=3.3.1" }, { name = "py-key-value-shared", editable = "key-value/key-value-shared" }, + { name = "pydantic", marker = "extra == 'all'", specifier = ">=2.11.9" }, { name = "pydantic", marker = "extra == 'pydantic'", specifier = ">=2.11.9" }, + { name = "pymongo", marker = "extra == 'all'", specifier = ">=4.0.0" }, { name = "pymongo", marker = "extra == 'mongodb'", specifier = ">=4.0.0" }, + { name = "pytz", marker = "extra == 'all'", specifier = ">=2025.2" }, { name = "pytz", marker = "extra == 'duckdb'", specifier = ">=2025.2" }, + { name = "redis", marker = "extra == 'all'", specifier = ">=4.3.0" }, { name = "redis", marker = "extra == 'redis'", specifier = ">=4.3.0" }, + { name = "rocksdict", marker = "python_full_version >= '3.12' and extra == 'all'", specifier = ">=0.3.24" }, { name = "rocksdict", marker = "python_full_version >= '3.12' and extra == 'rocksdb'", specifier = ">=0.3.24" }, + { name = "rocksdict", marker = "python_full_version < '3.12' and extra == 'all'", specifier = ">=0.3.2" }, { name = "rocksdict", marker = "python_full_version < '3.12' and extra == 'rocksdb'", specifier = ">=0.3.2" }, + { name = "types-hvac", marker = "extra == 'all'", specifier = ">=2.3.0" }, { name = "types-hvac", marker = "extra == 'vault'", specifier = ">=2.3.0" }, + { name = "valkey-glide-sync", marker = "extra == 'all'", specifier = ">=2.1.0" }, { name = "valkey-glide-sync", marker = "extra == 'valkey'", specifier = ">=2.1.0" }, ] -provides-extras = ["memory", "disk", "filetree", "redis", "mongodb", "valkey", "vault", "memcached", "elasticsearch", "pydantic", "keyring", "keyring-linux", "rocksdb", "duckdb", "wrappers-encryption"] +provides-extras = ["memory", "disk", "filetree", "redis", "mongodb", "valkey", "vault", "memcached", "elasticsearch", "pydantic", "keyring", "keyring-linux", "rocksdb", "duckdb", "firestore", "wrappers-encryption", "all"] [package.metadata.requires-dev] dev = [ { name = "py-key-value", extras = ["dev"], editable = "." }, { name = "py-key-value-sync", extras = ["keyring"] }, - { name = "py-key-value-sync", extras = ["memory", "disk", "filetree", "redis", "elasticsearch", "memcached", "mongodb", "vault", "rocksdb", "duckdb"] }, + { name = "py-key-value-sync", extras = ["memory", "disk", "filetree", "redis", "elasticsearch", "memcached", "mongodb", "vault", "rocksdb", "duckdb", "firestore"] }, { name = "py-key-value-sync", extras = ["pydantic"] }, { name = "py-key-value-sync", extras = ["valkey"], marker = "sys_platform != 'win32'" }, { name = "py-key-value-sync", extras = ["wrappers-encryption"] }, ] +[[package]] +name = "pyasn1" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322, upload-time = "2024-09-10T22:41:42.55Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135, upload-time = "2024-09-11T16:00:36.122Z" }, +] + +[[package]] +name = "pyasn1-modules" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892, upload-time = "2025-03-28T02:41:22.17Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, +] + [[package]] name = "pycparser" version = "2.23" @@ -2582,6 +2826,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9b/da/287023819144e3024e47c0379ca9a4f9e16d505342d3ad74ce9c9bb0541d/rocksdict-0.3.27-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:4b46d2dc9193aef558475ef8c12ddff8baaa0c006cc27ccc24c6fd4ecfbac2ee", size = 3789500, upload-time = "2025-05-27T17:16:58.888Z" }, ] +[[package]] +name = "rsa" +version = "4.9.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyasn1" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034, upload-time = "2025-04-16T09:51:18.218Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, +] + [[package]] name = "ruff" version = "0.14.2"