Skip to content
Merged
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
6 changes: 0 additions & 6 deletions .github/workflows/publish-py-kv-store-adapter.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
7 changes: 4 additions & 3 deletions .github/workflows/test_pull_request.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -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"
Expand Down
32 changes: 8 additions & 24 deletions src/kv_store_adapter/adapters/pydantic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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)
15 changes: 4 additions & 11 deletions src/kv_store_adapter/adapters/single_collection.py
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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)
8 changes: 6 additions & 2 deletions src/kv_store_adapter/stores/disk/store.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from pathlib import Path
from typing import Any, overload

from diskcache import Cache
Expand All @@ -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__()
Expand Down
6 changes: 3 additions & 3 deletions tests/adapters/test_pydantic.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
11 changes: 0 additions & 11 deletions tests/adapters/test_single_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.