Metadata-Version: 2.4 Name: py-key-value-aio Version: 0.4.5 Summary: Async Key-Value Store - A pluggable interface for KV Stores License-Expression: Apache-2.0 License-File: LICENSE Classifier: Development Status :: 3 - Alpha Classifier: Intended Audience :: Developers Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Requires-Dist: beartype>=0.20.0 Requires-Dist: typing-extensions>=4.15.0 Requires-Dist: aerospike>=16.0.0 ; extra == 'aerospike' Requires-Dist: azure-data-tables>=12.4.0 ; extra == 'azure-tables' Requires-Dist: azure-core>=1.29.0 ; extra == 'azure-tables' Requires-Dist: chdb>=4.1.1 ; extra == 'chdb' Requires-Dist: diskcache>=5.0.0 ; extra == 'disk' Requires-Dist: pathvalidate>=3.3.1 ; extra == 'disk' Requires-Dist: mkdocs>=1.6.0 ; extra == 'docs' Requires-Dist: mkdocs-material>=9.5.0 ; extra == 'docs' Requires-Dist: mkdocstrings[python]>=0.30.0 ; extra == 'docs' Requires-Dist: mkdocstrings-python>=1.10.0 ; extra == 'docs' Requires-Dist: duckdb>=1.1.1 ; extra == 'duckdb' Requires-Dist: pytz>=2025.2 ; extra == 'duckdb' Requires-Dist: aioboto3>=13.3.0 ; extra == 'dynamodb' Requires-Dist: types-aiobotocore-dynamodb>=2.16.0 ; extra == 'dynamodb' Requires-Dist: elasticsearch>=8.0.0 ; extra == 'elasticsearch' Requires-Dist: aiohttp>=3.12 ; extra == 'elasticsearch' Requires-Dist: aiofile>=3.5.0 ; extra == 'filetree' Requires-Dist: anyio>=4.4.0 ; extra == 'filetree' Requires-Dist: google-cloud-firestore>=2.13.0 ; extra == 'firestore' Requires-Dist: google-auth>=2.24.0 ; extra == 'firestore' Requires-Dist: keyring>=25.6.0 ; extra == 'keyring' Requires-Dist: keyring>=25.6.0 ; extra == 'keyring-linux' Requires-Dist: dbus-python>=1.4.0 ; extra == 'keyring-linux' Requires-Dist: aiomcache>=0.8.0 ; extra == 'memcached' Requires-Dist: cachetools>=5.0.0 ; extra == 'memory' Requires-Dist: pymongo>=4.9.0 ; extra == 'mongodb' Requires-Dist: opensearch-py[async]>=2.0.0 ; extra == 'opensearch' Requires-Dist: asyncpg>=0.30.0 ; extra == 'postgresql' Requires-Dist: pydantic>=2.11.9 ; extra == 'pydantic' Requires-Dist: redis>=4.3.0 ; extra == 'redis' Requires-Dist: rocksdict>=0.3.24 ; python_full_version >= '3.12' and extra == 'rocksdb' Requires-Dist: rocksdict>=0.3.2 ; python_full_version < '3.12' and extra == 'rocksdb' Requires-Dist: aioboto3>=13.3.0 ; extra == 's3' Requires-Dist: types-aiobotocore-s3>=2.16.0 ; extra == 's3' Requires-Dist: valkey-glide>=2.1.0 ; extra == 'valkey' Requires-Dist: hvac>=2.3.0 ; extra == 'vault' Requires-Dist: types-hvac>=2.3.0 ; extra == 'vault' Requires-Dist: cryptography>=45.0.0 ; extra == 'wrappers-encryption' Requires-Python: >=3.10 Provides-Extra: aerospike Provides-Extra: azure-tables Provides-Extra: chdb Provides-Extra: disk Provides-Extra: docs Provides-Extra: duckdb Provides-Extra: dynamodb Provides-Extra: elasticsearch Provides-Extra: filetree Provides-Extra: firestore Provides-Extra: keyring Provides-Extra: keyring-linux Provides-Extra: memcached Provides-Extra: memory Provides-Extra: mongodb Provides-Extra: opensearch Provides-Extra: postgresql Provides-Extra: pydantic Provides-Extra: redis Provides-Extra: rocksdb Provides-Extra: s3 Provides-Extra: valkey Provides-Extra: vault Provides-Extra: wrappers-encryption Description-Content-Type: text/markdown # Python Key-Value Libraries This monorepo contains: - `py-key-value-aio`: Async key-value store library (primary and supported). - `py-key-value-sync`: Sync version (no longer planned; async-only focus). ## Documentation - [Full Documentation](https://strawgate.com/py-key-value/) - [Getting Started Guide](https://strawgate.com/py-key-value/getting-started/) - [Stores Guide](https://strawgate.com/py-key-value/stores/) - [Wrappers Guide](https://strawgate.com/py-key-value/wrappers/) - [Adapters Guide](https://strawgate.com/py-key-value/adapters/) - [API Reference](https://strawgate.com/py-key-value/api/protocols/) ## Why use this library? - **Multiple backends**: Aerospike, Azure Tables, chDB, DuckDB, DynamoDB, Elasticsearch, FileTree, Firestore, Keyring, Memcached, MongoDB, OpenSearch, PostgreSQL, Redis, RocksDB, S3, Valkey, Vault, Windows Registry, 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 - **Wrappers**: Statistics tracking and extensible wrapper system - **Collection-based**: Organize keys into logical collections/namespaces - **Pluggable architecture**: Easy to add custom store implementations ## Value to Framework Authors While key-value storage is valuable for individual projects, its true power emerges when framework authors use it as a **pluggable abstraction layer**. By coding your framework against the `AsyncKeyValue` protocol, you enable your users to choose their own storage backend without changing a single line of your framework code. Users can seamlessly switch between local caching (memory, disk) for development and distributed storage (Redis, DynamoDB, MongoDB) for production. ### Real-World Example: FastMCP [FastMCP](https://github.com/jlowin/fastmcp) demonstrates this pattern perfectly. FastMCP framework authors use the `AsyncKeyValue` protocol for: - **Response caching middleware**: Store and retrieve cached responses - **OAuth proxy tokens**: Persist authentication tokens across sessions FastMCP users can plug in any store implementation: - Development: `MemoryStore()` for fast iteration - Production: `RedisStore()` for distributed caching - Testing: `NullStore()` for testing without side effects ### How to Use This in Your Framework 1. **Accept the protocol** in your framework's initialization: ```python from key_value.aio.protocols.key_value import AsyncKeyValue class YourFramework: def __init__(self, cache: AsyncKeyValue): self.cache = cache ``` 2. **Use simple key-value operations** in your framework: ```python # Store data await self.cache.put( key="session:123", value={"user_id": "456", "expires": "2024-01-01"}, collection="sessions", ttl=3600 ) # Retrieve data session = await self.cache.get(key="session:123", collection="sessions") ``` 3. **Let users choose their backend**: ```python # User's code - they control the storage backend from your_framework import YourFramework from key_value.aio.stores.redis import RedisStore from key_value.aio.stores.memory import MemoryStore # Development framework = YourFramework(cache=MemoryStore()) # Production framework = YourFramework( cache=RedisStore(url="redis://localhost:6379/0") ) # Production with Redis TLS framework = YourFramework( cache=RedisStore( url="rediss://redis.example.com:6380/0", ssl_ca_certs="/etc/ssl/certs/redis-ca.pem", ) ) ``` By depending on `py-key-value-aio` instead of a specific storage backend, you give your users the flexibility to choose the right storage for their needs while keeping your framework code clean and backend-agnostic. ## Why not use this library? - **Async-only**: This library focuses exclusively on async/await patterns. A synchronous wrapper library is not currently planned. - **Managed Entries**: Raw values are not stored in backends, a wrapper object is stored instead. This wrapper object contains the value, sometimes metadata like the TTL, and the creation timestamp. Most often it is serialized to and from JSON. - **No Live Objects**: Even when using the in-memory store, "live" objects are never returned from the store. You get a dictionary or a Pydantic model, hopefully a copy of what you stored, but never the same instance in memory. - **Dislike of Bear Bros**: Beartype is used for runtime type checking. Core protocol methods in store and wrapper implementations (put/get/delete/ttl and their batch variants) enforce types and will raise TypeError for violations. Other code produces warnings. You can disable all beartype checks by setting `PY_KEY_VALUE_DISABLE_BEARTYPE=true` or suppress warnings via the warnings module. ## Installation ## Quick start for Async library Install the library with the backends you need. ```bash # Async library pip install py-key-value-aio # With specific backend extras 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[s3] pip install py-key-value-aio[azure-tables] pip install py-key-value-aio[elasticsearch] pip install py-key-value-aio[firestore] # or: aerospike, redis, mongodb, memcached, valkey, vault, registry, rocksdb, # see below for all options ``` ```python import asyncio from key_value.aio.protocols.key_value import AsyncKeyValue from key_value.aio.stores.memory import MemoryStore async def example(key_value: AsyncKeyValue) -> None: await key_value.put(key="123", value={"name": "Alice"}, collection="users", ttl=3600) value = await key_value.get(key="123", collection="users") await key_value.delete(key="123", collection="users") async def main(): memory_store = MemoryStore() await example(key_value=memory_store) asyncio.run(main()) ``` ## Introduction to py-key-value ### Protocols - **Async**: `key_value.aio.protocols.AsyncKeyValue` — async `get/put/delete/ttl` and bulk variants; optional protocol segments for culling, destroying stores/collections, and enumerating keys/collections implemented by capable stores. The protocols offer a simple interface for your application to interact with the store: ```python get(key: str, collection: str | None = None) -> dict[str, Any] | None: get_many(keys: list[str], collection: str | None = None) -> list[dict[str, Any] | None]: put(key: str, value: dict[str, Any], collection: str | None = None, ttl: SupportsFloat | None = None) -> None: put_many(keys: list[str], values: Sequence[dict[str, Any]], collection: str | None = None, ttl: SupportsFloat | None = None) -> None: delete(key: str, collection: str | None = None) -> bool: delete_many(keys: list[str], collection: str | None = None) -> int: ttl(key: str, collection: str | None = None) -> tuple[dict[str, Any] | None, float | None]: ttl_many(keys: list[str], collection: str | None = None) -> list[tuple[dict[str, Any] | None, float | None]]: ``` ### Stores The library provides multiple store implementations organized into three categories: - **Local stores**: In-memory and disk-based storage (Memory, Disk, RocksDB, etc.) - **Secret stores**: Secure OS-level storage for sensitive data (Keyring, Vault) - **Distributed stores**: Network-based storage for multi-node apps (Redis, DynamoDB, S3, Azure Tables, MongoDB, etc.) Each store has a **stability rating** indicating likelihood of backwards-incompatible changes. Stable stores (Redis, Valkey, Disk, Keyring) are recommended for long-term storage. **[📚 View all stores, installation guides, and examples →](https://strawgate.com/py-key-value/stores/)** ### Adapters Adapters provide specialized interfaces for working with stores. Unlike wrappers, they don't implement the protocol but instead offer alternative APIs for specific use cases: - **DataclassAdapter**: Type-safe dataclass storage with automatic validation - **PydanticAdapter**: Type-safe Pydantic model storage with serialization - **RaiseOnMissingAdapter**: Raise exceptions instead of returning None for missing keys **[📚 View all adapters with examples →](https://strawgate.com/py-key-value/adapters/)** **Quick example** - PydanticAdapter for type-safe storage: ```python import asyncio from pydantic import BaseModel from key_value.aio.adapters.pydantic import PydanticAdapter from key_value.aio.stores.memory import MemoryStore class User(BaseModel): name: str email: str async def example(): memory_store: MemoryStore = MemoryStore() user_adapter: PydanticAdapter[User] = PydanticAdapter( key_value=memory_store, pydantic_model=User, default_collection="users", ) new_user: User = User(name="John Doe", email="john.doe@example.com") # Directly store the User model await user_adapter.put( key="john-doe", value=new_user, ) # Retrieve the User model existing_user: User | None = await user_adapter.get( key="john-doe", ) asyncio.run(example()) ``` ### Wrappers Wrappers add functionality to stores while implementing the protocol themselves, allowing them to be stacked and used anywhere a store is expected. Available wrappers include: - **Performance**: Compression, Caching (Passthrough), Statistics, Timeout - **Security**: Encryption (Fernet), ReadOnly - **Reliability**: Retry, Fallback - **Routing**: CollectionRouting, Routing, SingleCollection - **Organization**: PrefixKeys, PrefixCollections - **Constraints**: LimitSize, TTLClamp, DefaultValue - **Observability**: Logging, Statistics **[📚 View all wrappers with examples →](https://strawgate.com/py-key-value/wrappers/)** Wrappers can be stacked for complex functionality: ```python # Create a retriable redis store with timeout protection that is monitored, # with compressed values, and a fallback to memory store! This probably isn't # a good idea but you can do it! store = LoggingWrapper( CompressionWrapper( FallbackWrapper( primary_key_value=RetryWrapper( TimeoutWrapper( key_value=redis_store, ) ), fallback_key_value=memory_store, ) ) ) ``` Wrappers are applied in order, so the outermost wrapper is applied first and the innermost wrapper is applied last. Keep this in mind when chaining wrappers! ### Atomicity / Consistency We aim for consistent semantics across basic key-value operations. Guarantees may vary by backend (especially distributed systems) and for bulk or management operations. ## Advanced Patterns Adapters, stores, and wrappers can be combined in a variety of ways as needed. The following example simulates a consumer of your service providing an Elasticsearch store and forcing all data into a single collection. They pass this wrapped store to your service and you further wrap it in a statistics wrapper (for metrics/monitoring) and a pydantic adapter, to simplify the application's usage. ```python import asyncio from pydantic import BaseModel from key_value.aio.adapters.pydantic import PydanticAdapter from key_value.aio.wrappers.single_collection import SingleCollectionWrapper from key_value.aio.wrappers.statistics import StatisticsWrapper from key_value.aio.stores.elasticsearch import ElasticsearchStore class User(BaseModel): name: str email: str elasticsearch_store: ElasticsearchStore = ElasticsearchStore( url="https://localhost:9200", api_key="your-api-key", index="kv-store" ) single_collection: SingleCollectionWrapper = SingleCollectionWrapper( key_value=elasticsearch_store, single_collection="users", default_collection="one-collection" ) async def main(key_value: AsyncKeyValue): statistics_wrapper = StatisticsWrapper(key_value=key_value) users = PydanticAdapter(key_value=statistics_wrapper, pydantic_model=User) await users.put( key="u1", value=User(name="Jane", email="j@example.com"), collection="ignored" ) user = await users.get(key="u1", collection="ignored") _ = statistics_wrapper.statistics # access metrics asyncio.run(main(key_value=single_collection)) ``` ## Project links - [Full Documentation](https://strawgate.com/py-key-value/) - [API Reference](https://strawgate.com/py-key-value/api/protocols/) Contributions welcome but may not be accepted. File an issue before submitting a pull request. If you do not get agreement on your proposal before making a pull request you may have a bad time. Apache 2.0 licensed.