From 282600cb225aee69180a0309ab80b216e3d5a91d Mon Sep 17 00:00:00 2001 From: William Easton Date: Wed, 24 Sep 2025 12:31:45 -0500 Subject: [PATCH 1/4] Switch adapters to using the protocol instead of stores --- README.md | 4 +-- pyproject.toml | 2 +- src/kv_store_adapter/adapters/pydantic.py | 32 +++++-------------- .../adapters/single_collection.py | 15 +++------ src/kv_store_adapter/stores/disk/store.py | 8 +++-- tests/adapters/test_pydantic.py | 6 ++-- tests/adapters/test_single_collection.py | 11 ------- 7 files changed, 24 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 140ddc0a..3c576436 100644 --- a/README.md +++ b/README.md @@ -77,9 +77,9 @@ For detailed configuration options and all available stores, see [DEVELOPING.md] We strive to support atomicity and consistency across all stores and operations in the KVStoreProtocol. That being said, there are operations available via the BaseKVStore class which are management operations like listing keys, listing collections, clearing collections, culling expired entries, etc. These operations may not be atomic, may be eventually consistent across stores, or may have other limitations (like limited to returning a certain number of keys). -## Adapters +## Protocol Adapters -The library provides an adapter pattern simplifying the user of the protocol/store. Adapters themselves do not implement the `KVStoreProtocol` interface and cannot be nested. Adapters can be used with wrappers and stores interchangeably. +The library provides an adapter pattern simplifying the use of the protocol/store. Adapters themselves do not implement the `KVStoreProtocol` interface and cannot be nested. Adapters can be used with anything that implements the `KVStoreProtocol` interface but do not compliant with the full `BaseKVStore` interface and thus lack management operations like listing keys, listing collections, clearing collections, culling expired entries, etc. The following adapters are available: diff --git a/pyproject.toml b/pyproject.toml index ebc09da6..f33d0610 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "kv-store-adapter" -version = "0.1.0" +version = "0.1.1" description = "A pluggable interface for KV Stores" readme = "README.md" requires-python = ">=3.10" diff --git a/src/kv_store_adapter/adapters/pydantic.py b/src/kv_store_adapter/adapters/pydantic.py index 17bc524c..5cc90922 100644 --- a/src/kv_store_adapter/adapters/pydantic.py +++ b/src/kv_store_adapter/adapters/pydantic.py @@ -4,21 +4,20 @@ from pydantic_core import PydanticSerializationError from kv_store_adapter.errors import DeserializationError, SerializationError -from kv_store_adapter.stores.base.unmanaged import BaseKVStore -from kv_store_adapter.types import TTLInfo +from kv_store_adapter.types import KVStoreProtocol T = TypeVar("T", bound=BaseModel) class PydanticAdapter(Generic[T]): - """Adapter around a KV Store that allows type-safe persistence of Pydantic models.""" + """Adapter around a KVStoreProtocol-compliant Store that allows type-safe persistence of Pydantic models.""" - def __init__(self, store: BaseKVStore, pydantic_model: type[T]) -> None: - self.store: BaseKVStore = store + def __init__(self, store_protocol: KVStoreProtocol, pydantic_model: type[T]) -> None: + self.store_protocol: KVStoreProtocol = store_protocol self.pydantic_model: type[T] = pydantic_model async def get(self, collection: str, key: str) -> T | None: - if value := await self.store.get(collection=collection, key=key): + if value := await self.store_protocol.get(collection=collection, key=key): try: return self.pydantic_model.model_validate(obj=value) except ValidationError as e: @@ -34,25 +33,10 @@ async def put(self, collection: str, key: str, value: T, *, ttl: float | None = msg = f"Invalid Pydantic model: {e}" raise SerializationError(msg) from e - await self.store.put(collection=collection, key=key, value=value_dict, ttl=ttl) + await self.store_protocol.put(collection=collection, key=key, value=value_dict, ttl=ttl) async def delete(self, collection: str, key: str) -> bool: - return await self.store.delete(collection=collection, key=key) + return await self.store_protocol.delete(collection=collection, key=key) async def exists(self, collection: str, key: str) -> bool: - return await self.store.exists(collection=collection, key=key) - - async def keys(self, collection: str) -> list[str]: - return await self.store.keys(collection=collection) - - async def clear_collection(self, collection: str) -> int: - return await self.store.clear_collection(collection=collection) - - async def ttl(self, collection: str, key: str) -> TTLInfo | None: - return await self.store.ttl(collection=collection, key=key) - - async def list_collections(self) -> list[str]: - return await self.store.list_collections() - - async def cull(self) -> None: - await self.store.cull() + return await self.store_protocol.exists(collection=collection, key=key) diff --git a/src/kv_store_adapter/adapters/single_collection.py b/src/kv_store_adapter/adapters/single_collection.py index c12e3904..23ea6f6e 100644 --- a/src/kv_store_adapter/adapters/single_collection.py +++ b/src/kv_store_adapter/adapters/single_collection.py @@ -1,14 +1,13 @@ from typing import Any -from kv_store_adapter.stores.base.unmanaged import BaseKVStore -from kv_store_adapter.types import TTLInfo +from kv_store_adapter.types import KVStoreProtocol class SingleCollectionAdapter: - """Adapter around a KV Store that only allows one collection.""" + """Adapter around a KVStoreProtocol-compliant Store that only allows one collection.""" - def __init__(self, store: BaseKVStore, collection: str) -> None: - self.store: BaseKVStore = store + def __init__(self, store: KVStoreProtocol, collection: str) -> None: + self.store: KVStoreProtocol = store self.collection: str = collection async def get(self, key: str) -> dict[str, Any] | None: @@ -22,9 +21,3 @@ async def delete(self, key: str) -> bool: async def exists(self, key: str) -> bool: return await self.store.exists(collection=self.collection, key=key) - - async def keys(self) -> list[str]: - return await self.store.keys(collection=self.collection) - - async def ttl(self, key: str) -> TTLInfo | None: - return await self.store.ttl(collection=self.collection, key=key) diff --git a/src/kv_store_adapter/stores/disk/store.py b/src/kv_store_adapter/stores/disk/store.py index e84a31ed..41a9d49b 100644 --- a/src/kv_store_adapter/stores/disk/store.py +++ b/src/kv_store_adapter/stores/disk/store.py @@ -1,3 +1,4 @@ +from pathlib import Path from typing import Any, overload from diskcache import Cache @@ -20,15 +21,18 @@ class DiskStore(BaseManagedKVStore): def __init__(self, *, cache: Cache) -> None: ... @overload - def __init__(self, *, path: str, size_limit: int | None = None) -> None: ... + def __init__(self, *, path: Path | str, size_limit: int | None = None) -> None: ... - def __init__(self, *, cache: Cache | None = None, path: str | None = None, size_limit: int | None = None) -> None: + def __init__(self, *, cache: Cache | None = None, path: Path | str | None = None, size_limit: int | None = None) -> None: """Initialize the in-memory cache. Args: disk_cache: The disk cache to use. size_limit: The maximum size of the disk cache. Defaults to 1GB. """ + if isinstance(path, Path): + path = str(object=path) + self._cache = cache or Cache(directory=path, size_limit=size_limit or DEFAULT_DISK_STORE_SIZE_LIMIT) super().__init__() diff --git a/tests/adapters/test_pydantic.py b/tests/adapters/test_pydantic.py index 912c71b1..b2bf81df 100644 --- a/tests/adapters/test_pydantic.py +++ b/tests/adapters/test_pydantic.py @@ -42,15 +42,15 @@ async def store(self) -> MemoryStore: @pytest.fixture async def user_adapter(self, store: MemoryStore) -> PydanticAdapter[User]: - return PydanticAdapter[User](store=store, pydantic_model=User) + return PydanticAdapter[User](store_protocol=store, pydantic_model=User) @pytest.fixture async def product_adapter(self, store: MemoryStore) -> PydanticAdapter[Product]: - return PydanticAdapter[Product](store=store, pydantic_model=Product) + return PydanticAdapter[Product](store_protocol=store, pydantic_model=Product) @pytest.fixture async def order_adapter(self, store: MemoryStore) -> PydanticAdapter[Order]: - return PydanticAdapter[Order](store=store, pydantic_model=Order) + return PydanticAdapter[Order](store_protocol=store, pydantic_model=Order) async def test_simple_adapter(self, user_adapter: PydanticAdapter[User]): await user_adapter.put(collection="test", key="test", value=SAMPLE_USER) diff --git a/tests/adapters/test_single_collection.py b/tests/adapters/test_single_collection.py index c13b35ab..8c18b7d2 100644 --- a/tests/adapters/test_single_collection.py +++ b/tests/adapters/test_single_collection.py @@ -26,14 +26,3 @@ async def test_put_exists_delete_exists(self, adapter: SingleCollectionAdapter): assert await adapter.exists(key="test") assert await adapter.delete(key="test") assert await adapter.exists(key="test") is False - - async def test_put_keys(self, adapter: SingleCollectionAdapter): - await adapter.put(key="test", value={"test": "test"}) - assert await adapter.keys() == ["test"] - - async def test_put_keys_delete_keys_get(self, adapter: SingleCollectionAdapter): - await adapter.put(key="test", value={"test": "test"}) - assert await adapter.keys() == ["test"] - assert await adapter.delete(key="test") - assert await adapter.keys() == [] - assert await adapter.get(key="test") is None From a6c235656df25799e48d0f10d5a8dac417f1ad3a Mon Sep 17 00:00:00 2001 From: William Easton Date: Wed, 24 Sep 2025 12:32:49 -0500 Subject: [PATCH 2/4] Update test workflows --- .github/workflows/publish-py-kv-store-adapter.yml | 6 ------ .github/workflows/test_pull_request.yml | 7 ++++--- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/.github/workflows/publish-py-kv-store-adapter.yml b/.github/workflows/publish-py-kv-store-adapter.yml index b94f5fe9..0ff28ab8 100644 --- a/.github/workflows/publish-py-kv-store-adapter.yml +++ b/.github/workflows/publish-py-kv-store-adapter.yml @@ -3,12 +3,6 @@ name: Publish py-kv-store-adapter to PyPI on: release: types: [created] - push: - branches: - - main - pull_request: - branches: - - main workflow_dispatch: jobs: diff --git a/.github/workflows/test_pull_request.yml b/.github/workflows/test_pull_request.yml index 16d538c2..1c5e5691 100644 --- a/.github/workflows/test_pull_request.yml +++ b/.github/workflows/test_pull_request.yml @@ -1,11 +1,12 @@ -name: Run tests for pull requests +name: Run tests for pull requests and merges on: - release: - types: [created] pull_request: branches: - main + push: + branches: + - main workflow_dispatch: jobs: From a7365c65679f467c6f222b0ec7a4e5fca23f51ec Mon Sep 17 00:00:00 2001 From: William Easton Date: Wed, 24 Sep 2025 12:33:32 -0500 Subject: [PATCH 3/4] Apply suggestions from code review Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3c576436..78f7f767 100644 --- a/README.md +++ b/README.md @@ -79,7 +79,7 @@ there are operations available via the BaseKVStore class which are management op ## Protocol Adapters -The library provides an adapter pattern simplifying the use of the protocol/store. Adapters themselves do not implement the `KVStoreProtocol` interface and cannot be nested. Adapters can be used with anything that implements the `KVStoreProtocol` interface but do not compliant with the full `BaseKVStore` interface and thus lack management operations like listing keys, listing collections, clearing collections, culling expired entries, etc. +The library provides an adapter pattern simplifying the use of the protocol/store. Adapters themselves do not implement the `KVStoreProtocol` interface and cannot be nested. Adapters can be used with anything that implements the `KVStoreProtocol` interface but do not comply with the full `BaseKVStore` interface and thus lack management operations like listing keys, listing collections, clearing collections, culling expired entries, etc. The following adapters are available: From 3743d6c7b156338f40643bab8b0a52c63c8d4209 Mon Sep 17 00:00:00 2001 From: William Easton Date: Wed, 24 Sep 2025 12:34:32 -0500 Subject: [PATCH 4/4] Bump lock --- uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index 092b2ccb..28fe46cc 100644 --- a/uv.lock +++ b/uv.lock @@ -402,7 +402,7 @@ wheels = [ [[package]] name = "kv-store-adapter" -version = "0.1.0" +version = "0.1.1" source = { editable = "." } [package.optional-dependencies]