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: diff --git a/README.md b/README.md index 140ddc0a..78f7f767 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 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: 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 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]