-
Notifications
You must be signed in to change notification settings - Fork 6
Add FirestoreStore backend (aio + generated sync) #249
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
abd80e2
6dc1528
11e098d
a84c4ab
d1c00fb
715251c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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"] | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
# Check latest versions of firestore dependencies
curl -s https://pypi.org/pypi/google-cloud-firestore/json | jq '.info.version'
curl -s https://pypi.org/pypi/google-auth/json | jq '.info.version'Repository: strawgate/py-key-value Length of output: 83 🏁 Script executed: #!/bin/bash
# Find and examine pyproject.toml to see other store backend patterns
cd key-value/key-value-aio
cat -n pyproject.toml | head -80Repository: strawgate/py-key-value Length of output: 3015 Consider updating the firestore dependency versions. The specified constraints are functional but outdated— 🤖 Prompt for AI Agents |
||
| 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]", | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| """Firestore key-value store.""" | ||
|
|
||
| from key_value.aio.stores.firestore.store import FirestoreStore | ||
|
|
||
| __all__ = ["FirestoreStore"] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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() |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| """Tests for Firestore store.""" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix punctuation per American English style.
The static analysis tool correctly flags that "etc." requires a period in American English.
Apply this diff:
📝 Committable suggestion
🧰 Tools
🪛 LanguageTool
[style] ~21-~21: In American English, abbreviations like “etc.” require a period.
Context: ...Valkey, Firestore, and In-memory, Disk, etc - TTL support: Automatic expiration...
(ETC_PERIOD)
🤖 Prompt for AI Agents