Skip to content

Commit 2af53c3

Browse files
authored
Merge branch 'main' into claude/issue-115-20251026-0511
2 parents 1ac0750 + 8151b4f commit 2af53c3

File tree

37 files changed

+716
-49
lines changed

37 files changed

+716
-49
lines changed

.devcontainer/devcontainer.json

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
{
2+
"name": "py-key-value",
3+
"image": "ghcr.io/astral-sh/uv:python3.10-bookworm",
4+
"features": {
5+
"ghcr.io/devcontainers/features/node:1": {
6+
"version": "lts"
7+
},
8+
"ghcr.io/devcontainers/features/github-cli:1": {},
9+
"ghcr.io/devcontainers/features/git:1": {}
10+
},
11+
"runArgs": [
12+
"--network=host"
13+
],
14+
"mounts": [
15+
"source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind"
16+
],
17+
"customizations": {
18+
"vscode": {
19+
"extensions": [
20+
"ms-python.python",
21+
"ms-python.vscode-pylance",
22+
"charliermarsh.ruff",
23+
"DavidAnson.vscode-markdownlint"
24+
],
25+
"settings": {
26+
"python.defaultInterpreterPath": "/usr/local/bin/python",
27+
"python.testing.pytestEnabled": true,
28+
"python.testing.unittestEnabled": false,
29+
"python.testing.pytestArgs": [
30+
"key-value",
31+
"--import-mode=importlib",
32+
"-vv"
33+
]
34+
}
35+
}
36+
},
37+
"postCreateCommand": "make sync",
38+
"remoteUser": "root"
39+
}

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ __pycache__/
66
# C extensions
77
*.so
88

9+
# MacOS junk
10+
.DS_Store
11+
912
# Distribution / packaging
1013
.Python
1114
build/

AGENTS.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,10 @@ pip install py-key-value-aio[mongodb] # MongoDB support
106106

107107
**Never edit files in `key-value/key-value-sync/` directly**. Any changes
108108
will be overwritten when `make codegen` runs. Always make changes in the
109-
async package and regenerate.
109+
async package and regenerate. Always run `make codegen` after making changes
110+
to the async package. You will need to include the generated code in your pull
111+
request. Nobody will generate it for you. This also means pull requests will contain
112+
two copies of your changes, this is intentional!
110113

111114
## Make Commands Reference
112115

Makefile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ ifdef PROJECT
7575
@cd $(PROJECT) && uv sync --locked --group dev
7676
else
7777
@echo "Syncing all packages..."
78-
@uv sync --all-packages
78+
@uv sync --all-packages --group dev
7979
@npm install -g markdownlint-cli
8080
endif
8181

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -296,6 +296,7 @@ The following wrappers are available:
296296

297297
| Wrapper | Description | Example |
298298
|---------|---------------|-----|
299+
| CollectionRoutingWrapper | Route operations to different stores based on a collection name. | `CollectionRoutingWrapper(collection_map={"sessions": redis_store, "users": dynamo_store}, default_store=memory_store)` |
299300
| CompressionWrapper | Compress values before storing and decompress on retrieval. | `CompressionWrapper(key_value=memory_store, min_size_to_compress=0)` |
300301
| FernetEncryptionWrapper | Encrypt values before storing and decrypt on retrieval. | `FernetEncryptionWrapper(key_value=memory_store, source_material="your-source-material", salt="your-salt")` |
301302
| FallbackWrapper | Fallback to a secondary store when the primary store fails. | `FallbackWrapper(primary_key_value=memory_store, fallback_key_value=memory_store)` |
@@ -306,6 +307,7 @@ The following wrappers are available:
306307
| PrefixKeysWrapper | Prefix all keys with a given prefix. | `PrefixKeysWrapper(key_value=memory_store, prefix="users")` |
307308
| ReadOnlyWrapper | Prevent all write operations on the underlying store. | `ReadOnlyWrapper(key_value=memory_store, raise_on_write=True)` |
308309
| RetryWrapper | Retry failed operations with exponential backoff. | `RetryWrapper(key_value=memory_store, max_retries=3, initial_delay=0.1, max_delay=10.0, exponential_base=2.0)` |
310+
| RoutingWrapper | Route operations to different stores based on a routing function. | `RoutingWrapper(routing_function=lambda collection: redis_store if collection == "sessions" else dynamo_store, default_store=memory_store)` |
309311
| SingleCollectionWrapper | Wrap a store to only use a single collection. | `SingleCollectionWrapper(key_value=memory_store, single_collection="users")` |
310312
| TTLClampWrapper | Clamp the TTL to a given range. | `TTLClampWrapper(key_value=memory_store, min_ttl=60, max_ttl=3600)` |
311313
| StatisticsWrapper | Track operation statistics for the store. | `StatisticsWrapper(key_value=memory_store)` |

key-value/key-value-aio/src/key_value/aio/adapters/pydantic/adapter.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,12 @@
1515
class PydanticAdapter(Generic[T]):
1616
"""Adapter around a KVStore-compliant Store that allows type-safe persistence of Pydantic models."""
1717

18+
_key_value: AsyncKeyValue
19+
_is_list_model: bool
20+
_type_adapter: TypeAdapter[T]
21+
_default_collection: str | None
22+
_raise_on_validation_error: bool
23+
1824
# Beartype doesn't like our `type[T] includes a bound on Sequence[...] as the subscript is not checkable at runtime
1925
# For just the next 20 or so lines we are no longer bear bros but have no fear, we will be back soon!
2026
@bear_spray
@@ -34,14 +40,14 @@ def __init__(
3440
raise_on_validation_error: Whether to raise a ValidationError if the model is invalid.
3541
"""
3642

37-
self._key_value: AsyncKeyValue = key_value
43+
self._key_value = key_value
3844

3945
origin = get_origin(pydantic_model)
40-
self._is_list_model: bool = origin is not None and issubclass(origin, Sequence)
46+
self._is_list_model = origin is not None and isinstance(origin, type) and issubclass(origin, Sequence)
4147

4248
self._type_adapter = TypeAdapter[T](pydantic_model)
43-
self._default_collection: str | None = default_collection
44-
self._raise_on_validation_error: bool = raise_on_validation_error
49+
self._default_collection = default_collection
50+
self._raise_on_validation_error = raise_on_validation_error
4551

4652
def _validate_model(self, value: dict[str, Any]) -> T | None:
4753
try:
@@ -60,7 +66,7 @@ def _serialize_model(self, value: T) -> dict[str, Any]:
6066
if self._is_list_model:
6167
return {"items": self._type_adapter.dump_python(value, mode="json")}
6268

63-
return self._type_adapter.dump_python(value, mode="json")
69+
return self._type_adapter.dump_python(value, mode="json") # pyright: ignore[reportAny]
6470
except PydanticSerializationError as e:
6571
msg = f"Invalid Pydantic model: {e}"
6672
raise SerializationError(msg) from e

key-value/key-value-aio/src/key_value/aio/stores/base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,7 @@ def __init__(self, *, default_collection: str | None = None) -> None:
7171
async def _setup(self) -> None:
7272
"""Initialize the store (called once before first use)."""
7373

74-
async def _setup_collection(self, *, collection: str) -> None: # pyright: ignore[reportUnusedParameter]
74+
async def _setup_collection(self, *, collection: str) -> None:
7575
"""Initialize the collection (called once before first use of the collection)."""
7676

7777
async def setup(self) -> None:

key-value/key-value-aio/src/key_value/aio/stores/disk/store.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ async def _get_managed_entry(self, *, key: str, collection: str) -> ManagedEntry
8585

8686
expire_epoch: float | None
8787

88-
managed_entry_str, expire_epoch = self._cache.get(key=combo_key, expire_time=True) # pyright: ignore[reportAny]
88+
managed_entry_str, expire_epoch = self._cache.get(key=combo_key, expire_time=True)
8989

9090
if not isinstance(managed_entry_str, str):
9191
return None

key-value/key-value-aio/src/key_value/aio/stores/dynamodb/store.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ def __init__(
101101
if client:
102102
self._client = client
103103
else:
104-
session: Session = aioboto3.Session( # pyright: ignore[reportAny]
104+
session: Session = aioboto3.Session(
105105
region_name=region_name,
106106
aws_access_key_id=aws_access_key_id,
107107
aws_secret_access_key=aws_secret_access_key,

key-value/key-value-aio/src/key_value/aio/stores/memory/store.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ def to_managed_entry(self) -> ManagedEntry:
4444
return ManagedEntry.from_json(json_str=self.json_str)
4545

4646

47-
def _memory_cache_ttu(_key: Any, value: MemoryCacheEntry, now: float) -> float: # pyright: ignore[reportAny]
47+
def _memory_cache_ttu(_key: Any, value: MemoryCacheEntry, now: float) -> float:
4848
"""Calculate time-to-use for cache entries based on their TTL."""
4949
if value.ttl_at_insert is None:
5050
return float(sys.maxsize)
@@ -56,7 +56,7 @@ def _memory_cache_ttu(_key: Any, value: MemoryCacheEntry, now: float) -> float:
5656
return float(expiration_epoch)
5757

5858

59-
def _memory_cache_getsizeof(value: MemoryCacheEntry) -> int: # pyright: ignore[reportUnusedParameter]
59+
def _memory_cache_getsizeof(value: MemoryCacheEntry) -> int: # noqa: ARG001
6060
"""Return size of cache entry (always 1 for entry counting)."""
6161
return 1
6262

0 commit comments

Comments
 (0)