Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Fix punctuation per American English style.

The static analysis tool correctly flags that "etc." requires a period in American English.

Apply this diff:

-- **Multiple backends**: DynamoDB, Elasticsearch, Memcached, MongoDB, Redis,
-  RocksDB, Valkey, Firestore, and In-memory, Disk, etc
+- **Multiple backends**: DynamoDB, Elasticsearch, Memcached, MongoDB, Redis,
+  RocksDB, Valkey, Firestore, and In-memory, Disk, etc.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
RocksDB, Valkey, Firestore, and In-memory, Disk, etc
RocksDB, Valkey, Firestore, and In-memory, Disk, etc.
🧰 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
In README.md around line 21, the list contains inconsistent capitalization and
is missing the period for "etc" per American English; change "RocksDB, Valkey,
Firestore, and In-memory, Disk, etc" to use consistent lowercase for the device
types and add the period — e.g. "RocksDB, Valkey, Firestore, and in-memory,
disk, etc." — ensuring the sentence ends with the period after "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
Expand Down Expand Up @@ -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
```

Expand Down
2 changes: 1 addition & 1 deletion docs/adapters.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
3 changes: 3 additions & 0 deletions docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
```
Expand Down
37 changes: 34 additions & 3 deletions docs/stores.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down Expand Up @@ -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 |

Expand Down Expand Up @@ -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 |
Expand Down Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion docs/wrappers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
3 changes: 2 additions & 1 deletion key-value/key-value-aio/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 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 -80

Repository: strawgate/py-key-value

Length of output: 3015


Consider updating the firestore dependency versions. The specified constraints are functional but outdated—google-cloud-firestore>=2.13.0 (current: 2.21.0) and google-auth>=2.24.0 (current: 2.43.0) have significantly newer versions available. Update to reflect the latest stable releases for better dependency freshness and potential bug fixes.

🤖 Prompt for AI Agents
In key-value/key-value-aio/pyproject.toml around line 53, the firestore
dependency constraints are outdated; update the version specifiers to reflect
current stable releases by changing "google-cloud-firestore>=2.13.0" to
"google-cloud-firestore>=2.21.0" and "google-auth>=2.24.0" to
"google-auth>=2.43.0" (or to the exact desired newer pinned versions) so the
project uses the latest compatible packages.

wrappers-encryption = ["cryptography>=45.0.0"]

[tool.pytest.ini_options]
Expand All @@ -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]",
Expand Down
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"]
121 changes: 121 additions & 0 deletions key-value/key-value-aio/src/key_value/aio/stores/firestore/store.py
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()
1 change: 1 addition & 0 deletions key-value/key-value-aio/tests/stores/firestore/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Tests for Firestore store."""
Loading
Loading