Skip to content

Commit edc0d89

Browse files
Enhance docstrings across key-value-aio codebase (#145)
Co-authored-by: claude[bot] <41898282+claude[bot]@users.noreply.github.com> Co-authored-by: William Easton <strawgate@users.noreply.github.com>
1 parent 764b86a commit edc0d89

File tree

6 files changed

+148
-10
lines changed

6 files changed

+148
-10
lines changed

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,21 @@ def __init__(
5050
self._raise_on_validation_error = raise_on_validation_error
5151

5252
def _validate_model(self, value: dict[str, Any]) -> T | None:
53+
"""Validate and deserialize a dict into the configured Pydantic model.
54+
55+
This method handles both single models and list models. For list models, it expects the value
56+
to contain an "items" key with the list data, following the convention used by `_serialize_model`.
57+
If validation fails and `raise_on_validation_error` is False, returns None instead of raising.
58+
59+
Args:
60+
value: The dict to validate and convert to a Pydantic model.
61+
62+
Returns:
63+
The validated model instance, or None if validation fails and errors are suppressed.
64+
65+
Raises:
66+
DeserializationError: If validation fails and `raise_on_validation_error` is True.
67+
"""
5368
try:
5469
if self._is_list_model:
5570
return self._type_adapter.validate_python(value.get("items", []))
@@ -62,6 +77,22 @@ def _validate_model(self, value: dict[str, Any]) -> T | None:
6277
return None
6378

6479
def _serialize_model(self, value: T) -> dict[str, Any]:
80+
"""Serialize a Pydantic model to a dict for storage.
81+
82+
This method handles both single models and list models. For list models, it wraps the serialized
83+
list in a dict with an "items" key (e.g., {"items": [...]}) to ensure consistent dict-based storage
84+
format across all value types. This wrapping convention is expected by `_validate_model` during
85+
deserialization.
86+
87+
Args:
88+
value: The Pydantic model instance to serialize.
89+
90+
Returns:
91+
A dict representation of the model suitable for storage.
92+
93+
Raises:
94+
SerializationError: If the model cannot be serialized.
95+
"""
6596
try:
6697
if self._is_list_model:
6798
return {"items": self._type_adapter.dump_python(value, mode="json")}

key-value/key-value-aio/src/key_value/aio/protocols/key_value.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@
44

55
@runtime_checkable
66
class AsyncKeyValueProtocol(Protocol):
7-
"""A subset of KV operations: get/put/delete and TTL variants, including bulk calls."""
7+
"""A subset of KV operations: get/put/delete and TTL variants, including bulk calls.
8+
9+
This protocol defines the minimal contract for key-value store implementations. All methods are
10+
async and may raise exceptions on connection failures, validation errors, or other operational issues.
11+
Implementations should handle backend-specific errors appropriately.
12+
"""
813

914
async def get(
1015
self,
@@ -54,6 +59,9 @@ async def delete(self, key: str, *, collection: str | None = None) -> bool:
5459
Args:
5560
key: The key to delete the value from.
5661
collection: The collection to delete the value from. If no collection is provided, it will use the default collection.
62+
63+
Returns:
64+
True if the key was deleted, False if the key did not exist.
5765
"""
5866
...
5967

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

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,18 @@
3232

3333

3434
def _seed_to_frozen_seed_data(seed: SEED_DATA_TYPE) -> FROZEN_SEED_DATA_TYPE:
35+
"""Convert mutable seed data to an immutable frozen structure.
36+
37+
This function converts the nested mapping structure of seed data into immutable MappingProxyType
38+
objects at all levels. Using immutable structures prevents accidental modification of seed data
39+
after store initialization and ensures thread-safety.
40+
41+
Args:
42+
seed: The mutable seed data mapping: {collection: {key: {field: value}}}.
43+
44+
Returns:
45+
An immutable frozen version of the seed data using MappingProxyType.
46+
"""
3547
return MappingProxyType(
3648
{collection: MappingProxyType({key: MappingProxyType(value) for key, value in items.items()}) for collection, items in seed.items()}
3749
)
@@ -101,6 +113,15 @@ async def _seed_store(self) -> None:
101113
await self.put(key=key, value=dict(value), collection=collection)
102114

103115
async def setup(self) -> None:
116+
"""Initialize the store if not already initialized.
117+
118+
This method is called automatically before any store operations and uses a lock to ensure
119+
thread-safe lazy initialization. It can also be called manually to ensure the store is ready
120+
before performing operations. The setup process includes calling the `_setup()` hook and
121+
seeding the store with initial data if provided.
122+
123+
This method is idempotent - calling it multiple times has no additional effect after the first call.
124+
"""
104125
if not self._setup_complete:
105126
async with self._setup_lock:
106127
if not self._setup_complete:
@@ -116,6 +137,19 @@ async def setup(self) -> None:
116137
await self._seed_store()
117138

118139
async def setup_collection(self, *, collection: str) -> None:
140+
"""Initialize a specific collection if not already initialized.
141+
142+
This method is called automatically before any collection-specific operations and uses a per-collection
143+
lock to ensure thread-safe lazy initialization. It can also be called manually to ensure a collection
144+
is ready before performing operations on it. The setup process includes calling the `_setup_collection()`
145+
hook for store-specific collection initialization.
146+
147+
This method is idempotent - calling it multiple times for the same collection has no additional effect
148+
after the first call.
149+
150+
Args:
151+
collection: The name of the collection to initialize.
152+
"""
119153
await self.setup()
120154

121155
if not self._setup_collection_complete[collection]:

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

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,34 @@
3434

3535

3636
def document_to_managed_entry(document: dict[str, Any]) -> ManagedEntry:
37-
"""
38-
Convert a MongoDB document to a ManagedEntry.
37+
"""Convert a MongoDB document back to a ManagedEntry.
38+
39+
This function deserializes a MongoDB document (created by `managed_entry_to_document`) back to a
40+
ManagedEntry object, parsing the stringified value field and preserving all metadata.
41+
42+
Args:
43+
document: The MongoDB document to convert.
44+
45+
Returns:
46+
A ManagedEntry object reconstructed from the document.
3947
"""
4048
return ManagedEntry.from_dict(data=document, stringified_value=True)
4149

4250

4351
def managed_entry_to_document(key: str, managed_entry: ManagedEntry) -> dict[str, Any]:
44-
"""
45-
Convert a ManagedEntry to a MongoDB document.
52+
"""Convert a ManagedEntry to a MongoDB document for storage.
53+
54+
This function serializes a ManagedEntry to a MongoDB document format, including the key and all
55+
metadata (TTL, creation, and expiration timestamps). The value is stringified to ensure proper
56+
storage in MongoDB. The serialization is designed to preserve all entry information for round-trip
57+
conversion back to a ManagedEntry.
58+
59+
Args:
60+
key: The key associated with this entry.
61+
managed_entry: The ManagedEntry to serialize.
62+
63+
Returns:
64+
A MongoDB document dict containing the key, value, and all metadata.
4665
"""
4766
return {
4867
"key": key,
@@ -127,6 +146,18 @@ async def __aexit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None: #
127146
await self._client.__aexit__(exc_type, exc_val, exc_tb)
128147

129148
def _sanitize_collection_name(self, collection: str) -> str:
149+
"""Sanitize a collection name to meet MongoDB naming requirements.
150+
151+
MongoDB has specific requirements for collection names (length limits, allowed characters).
152+
This method ensures collection names are compliant by truncating to the maximum allowed length
153+
and replacing invalid characters with safe alternatives.
154+
155+
Args:
156+
collection: The collection name to sanitize.
157+
158+
Returns:
159+
A sanitized collection name that meets MongoDB requirements.
160+
"""
130161
return sanitize_string(value=collection, max_length=MAX_COLLECTION_LENGTH, allowed_characters=ALPHANUMERIC_CHARACTERS)
131162

132163
@override

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

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,32 @@
2121

2222

2323
def managed_entry_to_json(managed_entry: ManagedEntry) -> str:
24-
"""
25-
Convert a ManagedEntry to a JSON string.
24+
"""Convert a ManagedEntry to a JSON string for Redis storage.
25+
26+
This function serializes a ManagedEntry to JSON format including all metadata (TTL, creation,
27+
and expiration timestamps). The serialization is designed to preserve all entry information
28+
for round-trip conversion back to a ManagedEntry.
29+
30+
Args:
31+
managed_entry: The ManagedEntry to serialize.
32+
33+
Returns:
34+
A JSON string representation of the ManagedEntry with full metadata.
2635
"""
2736
return managed_entry.to_json(include_metadata=True, include_expiration=True, include_creation=True)
2837

2938

3039
def json_to_managed_entry(json_str: str) -> ManagedEntry:
31-
"""
32-
Convert a JSON string to a ManagedEntry.
40+
"""Convert a JSON string from Redis storage back to a ManagedEntry.
41+
42+
This function deserializes a JSON string (created by `managed_entry_to_json`) back to a
43+
ManagedEntry object, preserving all metadata including TTL, creation, and expiration timestamps.
44+
45+
Args:
46+
json_str: The JSON string to deserialize.
47+
48+
Returns:
49+
A ManagedEntry object reconstructed from the JSON string.
3350
"""
3451
return ManagedEntry.from_json(json_str=json_str, includes_metadata=True)
3552

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

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,24 @@
88

99

1010
class BaseWrapper(AsyncKeyValue):
11-
"""A base wrapper for KVStore implementations that passes through to the underlying store."""
11+
"""A base wrapper for KVStore implementations that passes through to the underlying store.
12+
13+
This class implements the passthrough pattern where all operations are delegated to the wrapped
14+
key-value store without modification. It serves as a foundation for creating custom wrappers that
15+
need to intercept, modify, or enhance specific operations while passing through others unchanged.
16+
17+
To create a custom wrapper, subclass this class and override only the methods you need to customize.
18+
All other operations will automatically pass through to the underlying store.
19+
20+
Example:
21+
class LoggingWrapper(BaseWrapper):
22+
async def get(self, key: str, *, collection: str | None = None):
23+
logger.info(f"Getting key: {key}")
24+
return await super().get(key, collection=collection)
25+
26+
Attributes:
27+
key_value: The underlying AsyncKeyValue store that operations are delegated to.
28+
"""
1229

1330
key_value: AsyncKeyValue
1431

0 commit comments

Comments
 (0)