Skip to content

Commit 36f674c

Browse files
authored
Merge branch 'main' into claude/issue-11-20251026-2132
2 parents 62d2621 + d723faf commit 36f674c

File tree

48 files changed

+483
-82
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

48 files changed

+483
-82
lines changed

.github/workflows/claude-on-test-failure.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ jobs:
138138
"github-research-mcp"
139139
],
140140
"env": {
141-
"DISABLE_SUMMARIES": "true", # Disable verbose summaries for faster analysis
141+
"DISABLE_SUMMARIES": "true",
142142
"GITHUB_PERSONAL_ACCESS_TOKEN": "${{ secrets.GITHUB_TOKEN }}"
143143
}
144144
}

docs/api/index.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# API Reference
2+
3+
Complete API reference documentation for py-key-value.
4+
5+
## Overview
6+
7+
The py-key-value API is organized into four main components:
8+
9+
- **[Protocols](protocols.md)** - Core interfaces for the key-value store
10+
- **[Stores](stores.md)** - Backend implementations for different storage systems
11+
- **[Wrappers](wrappers.md)** - Decorators that add functionality to stores
12+
- **[Adapters](adapters.md)** - Utilities that simplify working with stores
13+
14+
## Quick Links
15+
16+
### Core Protocols
17+
18+
The [`AsyncKeyValue`](protocols.md) protocol defines the async interface that all
19+
stores implement.
20+
21+
The [`KeyValue`](protocols.md) protocol is the synchronous version.
22+
23+
### Popular Stores
24+
25+
- [MemoryStore](stores.md) - In-memory storage
26+
- [RedisStore](stores.md) - Redis backend
27+
- [DiskStore](stores.md) - File-based storage
28+
29+
### Common Wrappers
30+
31+
- [LoggingWrapper](wrappers.md) - Add logging to any store
32+
- [CacheWrapper](wrappers.md) - Add caching layer
33+
- [RetryWrapper](wrappers.md) - Add automatic retry logic
34+
35+
## Using the API Reference
36+
37+
Each page provides:
38+
39+
- **Type signatures** - Full type information for all parameters and return values
40+
- **Docstrings** - Detailed descriptions of functionality
41+
- **Source links** - View the implementation on GitHub
42+
- **Cross-references** - Navigate between related components
43+
44+
## Example Usage
45+
46+
```python
47+
from key_value.aio.stores.memory import MemoryStore
48+
from key_value.aio.wrappers.logging import LoggingWrapper
49+
50+
# Create a store with logging
51+
store = LoggingWrapper(MemoryStore())
52+
53+
# Use the store
54+
await store.put("key", "value")
55+
result = await store.get("key")
56+
```
57+
58+
For more examples and guides, see the [User Guide](../stores.md).

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,11 @@ async def _put_managed_entry(
132132
collection: str,
133133
managed_entry: ManagedEntry,
134134
) -> None:
135-
_ = self._cache[collection].set(key=key, value=self._serialization_adapter.dump_json(entry=managed_entry), expire=managed_entry.ttl)
135+
_ = self._cache[collection].set(
136+
key=key,
137+
value=self._serialization_adapter.dump_json(entry=managed_entry, key=key, collection=collection),
138+
expire=managed_entry.ttl,
139+
)
136140

137141
@override
138142
async def _delete_managed_entry(self, *, key: str, collection: str) -> bool:

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

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,11 @@ async def _put_managed_entry(
107107
) -> None:
108108
combo_key: str = compound_key(collection=collection, key=key)
109109

110-
_ = self._cache.set(key=combo_key, value=self._serialization_adapter.dump_json(entry=managed_entry), expire=managed_entry.ttl)
110+
_ = self._cache.set(
111+
key=combo_key,
112+
value=self._serialization_adapter.dump_json(entry=managed_entry, key=key, collection=collection),
113+
expire=managed_entry.ttl,
114+
)
111115

112116
@override
113117
async def _delete_managed_entry(self, *, key: str, collection: str) -> bool:

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
@@ -219,7 +219,7 @@ async def _put_managed_entry(
219219
managed_entry: ManagedEntry,
220220
) -> None:
221221
"""Store a managed entry in DynamoDB."""
222-
json_value = self._serialization_adapter.dump_json(entry=managed_entry)
222+
json_value = self._serialization_adapter.dump_json(entry=managed_entry, key=key, collection=collection)
223223

224224
item: dict[str, Any] = {
225225
"collection": {"S": collection},

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@
6767
"key": {
6868
"type": "keyword",
6969
},
70+
"version": {
71+
"type": "integer",
72+
},
7073
"value": {
7174
"properties": {
7275
"flattened": {
@@ -357,7 +360,7 @@ async def _put_managed_entry(
357360
index_name: str = self._get_index_name(collection=collection)
358361
document_id: str = self._get_document_id(key=key)
359362

360-
document: dict[str, Any] = self._serializer.dump_dict(entry=managed_entry)
363+
document: dict[str, Any] = self._serializer.dump_dict(entry=managed_entry, key=key, collection=collection)
361364

362365
try:
363366
_ = await self._client.index(
@@ -395,7 +398,7 @@ async def _put_managed_entries(
395398

396399
index_action: dict[str, Any] = new_bulk_action(action="index", index=index_name, document_id=document_id)
397400

398-
document: dict[str, Any] = self._serializer.dump_dict(entry=managed_entry)
401+
document: dict[str, Any] = self._serializer.dump_dict(entry=managed_entry, key=key, collection=collection)
399402

400403
operations.extend([index_action, document])
401404

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
"""Python keyring-based key-value store."""
22

3+
import os
4+
5+
from key_value.shared.errors.key_value import ValueTooLargeError
36
from key_value.shared.utils.compound import compound_key
47
from key_value.shared.utils.managed_entry import ManagedEntry
58
from key_value.shared.utils.sanitization import HybridSanitizationStrategy, SanitizationStrategy
@@ -17,6 +20,16 @@
1720

1821
DEFAULT_KEYCHAIN_SERVICE = "py-key-value"
1922

23+
24+
def is_value_too_large(value: bytes) -> bool:
25+
value_length = len(value)
26+
if os.name == "nt":
27+
return value_length > WINDOWS_MAX_VALUE_LENGTH
28+
return False
29+
30+
31+
WINDOWS_MAX_VALUE_LENGTH = 2560 # bytes
32+
2033
MAX_KEY_COLLECTION_LENGTH = 256
2134
ALLOWED_KEY_COLLECTION_CHARACTERS: str = ALPHANUMERIC_CHARACTERS
2235

@@ -105,7 +118,11 @@ async def _put_managed_entry(self, *, key: str, collection: str, managed_entry:
105118

106119
combo_key: str = compound_key(collection=sanitized_collection, key=sanitized_key)
107120

108-
json_str: str = self._serialization_adapter.dump_json(entry=managed_entry)
121+
json_str: str = self._serialization_adapter.dump_json(entry=managed_entry, key=key, collection=collection)
122+
encoded_json_bytes: bytes = json_str.encode(encoding="utf-8")
123+
124+
if is_value_too_large(value=encoded_json_bytes):
125+
raise ValueTooLargeError(size=len(encoded_json_bytes), max_size=2560, collection=sanitized_collection, key=sanitized_key)
109126

110127
keyring.set_password(service_name=self._service_name, username=combo_key, password=json_str)
111128

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ async def _put_managed_entry(
125125
else:
126126
exptime = max(int(managed_entry.ttl), 1)
127127

128-
json_value: str = self._serialization_adapter.dump_json(entry=managed_entry)
128+
json_value: str = self._serialization_adapter.dump_json(entry=managed_entry, key=key, collection=collection)
129129

130130
_ = await self._client.set(
131131
key=combo_key.encode(encoding="utf-8"),

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

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,7 @@ async def _setup_collection(self, *, collection: str) -> None:
219219
# Ensure index on the unique combo key and supporting queries
220220
sanitized_collection = self._sanitize_collection(collection=collection)
221221

222-
collection_filter: dict[str, str] = {"name": collection}
222+
collection_filter: dict[str, str] = {"name": sanitized_collection}
223223
matching_collections: list[str] = await self._db.list_collection_names(filter=collection_filter)
224224

225225
if matching_collections:
@@ -273,7 +273,7 @@ async def _put_managed_entry(
273273
collection: str,
274274
managed_entry: ManagedEntry,
275275
) -> None:
276-
mongo_doc = self._adapter.dump_dict(entry=managed_entry)
276+
mongo_doc = self._adapter.dump_dict(entry=managed_entry, key=key, collection=collection)
277277

278278
try:
279279
# Ensure that the value is serializable to JSON
@@ -308,7 +308,7 @@ async def _put_managed_entries(
308308

309309
operations: list[UpdateOne] = []
310310
for key, managed_entry in zip(keys, managed_entries, strict=True):
311-
mongo_doc = self._adapter.dump_dict(entry=managed_entry)
311+
mongo_doc = self._adapter.dump_dict(entry=managed_entry, key=key, collection=collection)
312312

313313
operations.append(
314314
UpdateOne(
@@ -346,8 +346,7 @@ async def _delete_collection(self, *, collection: str) -> bool:
346346

347347
_ = await self._db.drop_collection(name_or_collection=collection_name)
348348

349-
if collection_name in self._collections_by_name:
350-
del self._collections_by_name[collection]
349+
self._collections_by_name.pop(collection, None)
351350

352351
return True
353352

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -132,7 +132,7 @@ async def _put_managed_entry(
132132
) -> None:
133133
combo_key: str = compound_key(collection=collection, key=key)
134134

135-
json_value: str = self._adapter.dump_json(entry=managed_entry)
135+
json_value: str = self._adapter.dump_json(entry=managed_entry, key=key, collection=collection)
136136

137137
if managed_entry.ttl is not None:
138138
# Redis does not support <= 0 TTLs
@@ -160,7 +160,7 @@ async def _put_managed_entries(
160160
# If there is no TTL, we can just do a simple mset
161161
mapping: dict[str, str] = {}
162162
for key, managed_entry in zip(keys, managed_entries, strict=True):
163-
json_value = self._adapter.dump_json(entry=managed_entry)
163+
json_value = self._adapter.dump_json(entry=managed_entry, key=key, collection=collection)
164164
mapping[compound_key(collection=collection, key=key)] = json_value
165165

166166
await self._client.mset(mapping=mapping)
@@ -175,7 +175,7 @@ async def _put_managed_entries(
175175

176176
for key, managed_entry in zip(keys, managed_entries, strict=True):
177177
combo_key: str = compound_key(collection=collection, key=key)
178-
json_value = self._adapter.dump_json(entry=managed_entry)
178+
json_value = self._adapter.dump_json(entry=managed_entry, key=key, collection=collection)
179179

180180
pipeline.setex(name=combo_key, time=ttl_seconds, value=json_value)
181181

0 commit comments

Comments
 (0)