Skip to content

Commit e733fba

Browse files
committed
Run sync code-gen
1 parent 18bf082 commit e733fba

File tree

17 files changed

+427
-31
lines changed

17 files changed

+427
-31
lines changed

key-value/key-value-sync/src/key_value/sync/code_gen/adapters/pydantic/adapter.py

Lines changed: 50 additions & 1 deletion
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")}
@@ -91,6 +122,10 @@ def get(self, key: str, *, collection: str | None = None, default: T | None = No
91122
Raises:
92123
DeserializationError if the stored data cannot be validated as the model and the PydanticAdapter is configured to
93124
raise on validation error.
125+
126+
Note:
127+
When raise_on_validation_error=False and validation fails, returns the default value (which may be None).
128+
When raise_on_validation_error=True and validation fails, raises DeserializationError.
94129
"""
95130
collection = collection or self._default_collection
96131

@@ -121,6 +156,11 @@ def get_many(self, keys: Sequence[str], *, collection: str | None = None, defaul
121156
Raises:
122157
DeserializationError if the stored data cannot be validated as the model and the PydanticAdapter is configured to
123158
raise on validation error.
159+
160+
Note:
161+
When raise_on_validation_error=False and validation fails for any key, that position in the returned list
162+
will contain the default value (which may be None). The method returns a complete list matching the order
163+
and length of the input keys, with defaults substituted for missing or invalid entries.
124164
"""
125165
collection = collection or self._default_collection
126166

@@ -171,7 +211,16 @@ def delete_many(self, keys: Sequence[str], *, collection: str | None = None) ->
171211
def ttl(self, key: str, *, collection: str | None = None) -> tuple[T | None, float | None]:
172212
"""Get a model and its TTL seconds if present.
173213
174-
Returns (model, ttl_seconds) or (None, None) if missing.
214+
Args:
215+
key: The key to retrieve.
216+
collection: The collection to use. If not provided, uses the default collection.
217+
218+
Returns:
219+
A tuple of (model, ttl_seconds). Returns (None, None) if the key is missing or validation fails.
220+
221+
Note:
222+
When validation fails and raise_on_validation_error=False, returns (None, None) even if TTL data exists.
223+
When validation fails and raise_on_validation_error=True, raises DeserializationError.
175224
"""
176225
collection = collection or self._default_collection
177226

key-value/key-value-sync/src/key_value/sync/code_gen/protocols/key_value.py

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

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

1217
def get(self, key: str, *, collection: str | None = None) -> dict[str, Any] | None:
1318
"""Retrieve a value by key from the specified collection.
@@ -52,6 +57,9 @@ def delete(self, key: str, *, collection: str | None = None) -> bool:
5257
Args:
5358
key: The key to delete the value from.
5459
collection: The collection to delete the value from. If no collection is provided, it will use the default collection.
60+
61+
Returns:
62+
True if the key was deleted, False if the key did not exist.
5563
"""
5664
...
5765

key-value/key-value-sync/src/key_value/sync/code_gen/stores/base.py

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

3636

3737
def _seed_to_frozen_seed_data(seed: SEED_DATA_TYPE) -> FROZEN_SEED_DATA_TYPE:
38+
"""Convert mutable seed data to an immutable frozen structure.
39+
40+
This function converts the nested mapping structure of seed data into immutable MappingProxyType
41+
objects at all levels. Using immutable structures prevents accidental modification of seed data
42+
after store initialization and ensures thread-safety.
43+
44+
Args:
45+
seed: The mutable seed data mapping: {collection: {key: {field: value}}}.
46+
47+
Returns:
48+
An immutable frozen version of the seed data using MappingProxyType.
49+
"""
3850
return MappingProxyType(
3951
{
4052
collection: MappingProxyType({key: MappingProxyType(value) for (key, value) in items.items()})
@@ -107,6 +119,15 @@ def _seed_store(self) -> None:
107119
self.put(key=key, value=dict(value), collection=collection)
108120

109121
def setup(self) -> None:
122+
"""Initialize the store if not already initialized.
123+
124+
This method is called automatically before any store operations and uses a lock to ensure
125+
thread-safe lazy initialization. It can also be called manually to ensure the store is ready
126+
before performing operations. The setup process includes calling the `_setup()` hook and
127+
seeding the store with initial data if provided.
128+
129+
This method is idempotent - calling it multiple times has no additional effect after the first call.
130+
"""
110131
if not self._setup_complete:
111132
with self._setup_lock:
112133
if not self._setup_complete:
@@ -122,6 +143,19 @@ def setup(self) -> None:
122143
self._seed_store()
123144

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

127161
if not self._setup_collection_complete[collection]:

key-value/key-value-sync/src/key_value/sync/code_gen/stores/elasticsearch/store.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,19 @@
5959

6060

6161
def managed_entry_to_document(collection: str, key: str, managed_entry: ManagedEntry) -> dict[str, Any]:
62+
"""Convert a ManagedEntry to an Elasticsearch document.
63+
64+
This function creates an Elasticsearch document containing the collection, key,
65+
JSON-serialized value, and optional timestamp metadata.
66+
67+
Args:
68+
collection: The collection name.
69+
key: The entry key.
70+
managed_entry: The ManagedEntry to convert.
71+
72+
Returns:
73+
An Elasticsearch document dictionary.
74+
"""
6275
document: dict[str, Any] = {"collection": collection, "key": key, "value": managed_entry.to_json(include_metadata=False)}
6376

6477
if managed_entry.created_at:
@@ -70,6 +83,20 @@ def managed_entry_to_document(collection: str, key: str, managed_entry: ManagedE
7083

7184

7285
def source_to_managed_entry(source: dict[str, Any]) -> ManagedEntry:
86+
"""Convert an Elasticsearch document source to a ManagedEntry.
87+
88+
This function deserializes an Elasticsearch document back to a ManagedEntry,
89+
parsing the JSON-encoded value and timestamp metadata.
90+
91+
Args:
92+
source: The Elasticsearch document _source dictionary.
93+
94+
Returns:
95+
A ManagedEntry reconstructed from the document.
96+
97+
Raises:
98+
DeserializationError: If the value field is missing or not a valid string.
99+
"""
73100
if not (value_str := source.get("value")) or not isinstance(value_str, str):
74101
msg = "Value is not a string"
75102
raise DeserializationError(msg)

key-value/key-value-sync/src/key_value/sync/code_gen/stores/elasticsearch/utils.py

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,14 @@
88

99

1010
def get_body_from_response(response: ObjectApiResponse[Any]) -> dict[str, Any]:
11+
"""Extract and validate the body from an Elasticsearch response.
12+
13+
Args:
14+
response: The Elasticsearch API response object.
15+
16+
Returns:
17+
The response body as a dictionary, or an empty dict if the body is missing or invalid.
18+
"""
1119
if not (body := response.body): # pyright: ignore[reportAny]
1220
return {}
1321

@@ -18,6 +26,14 @@ def get_body_from_response(response: ObjectApiResponse[Any]) -> dict[str, Any]:
1826

1927

2028
def get_source_from_body(body: dict[str, Any]) -> dict[str, Any]:
29+
"""Extract and validate the _source field from an Elasticsearch response body.
30+
31+
Args:
32+
body: The response body dictionary from Elasticsearch.
33+
34+
Returns:
35+
The _source field as a dictionary, or an empty dict if missing or invalid.
36+
"""
2137
if not (source := body.get("_source")):
2238
return {}
2339

@@ -28,6 +44,14 @@ def get_source_from_body(body: dict[str, Any]) -> dict[str, Any]:
2844

2945

3046
def get_aggregations_from_body(body: dict[str, Any]) -> dict[str, Any]:
47+
"""Extract and validate the aggregations field from an Elasticsearch response body.
48+
49+
Args:
50+
body: The response body dictionary from Elasticsearch.
51+
52+
Returns:
53+
The aggregations field as a dictionary, or an empty dict if missing or invalid.
54+
"""
3155
if not (aggregations := body.get("aggregations")):
3256
return {}
3357

@@ -38,6 +62,17 @@ def get_aggregations_from_body(body: dict[str, Any]) -> dict[str, Any]:
3862

3963

4064
def get_hits_from_response(response: ObjectApiResponse[Any]) -> list[dict[str, Any]]:
65+
"""Extract and validate the hits array from an Elasticsearch response.
66+
67+
This function navigates the nested structure of Elasticsearch responses to extract
68+
the hits.hits array which contains the actual search results.
69+
70+
Args:
71+
response: The Elasticsearch API response object.
72+
73+
Returns:
74+
A list of hit dictionaries from the response, or an empty list if the hits are missing or invalid.
75+
"""
4176
if not (body := response.body): # pyright: ignore[reportAny]
4277
return []
4378

@@ -66,6 +101,21 @@ def get_hits_from_response(response: ObjectApiResponse[Any]) -> list[dict[str, A
66101

67102

68103
def get_fields_from_hit(hit: dict[str, Any]) -> dict[str, list[Any]]:
104+
"""Extract and validate the fields from an Elasticsearch hit.
105+
106+
Elasticsearch can return stored fields via the "fields" key in each hit.
107+
This function validates that the fields object exists and conforms to the
108+
expected structure (a dict mapping field names to lists of values).
109+
110+
Args:
111+
hit: A single hit dictionary from an Elasticsearch response.
112+
113+
Returns:
114+
The fields dictionary from the hit, or an empty dict if missing.
115+
116+
Raises:
117+
TypeError: If the fields structure is invalid (not a dict or contains non-list values).
118+
"""
69119
if not (fields := hit.get("fields")):
70120
return {}
71121

@@ -81,6 +131,18 @@ def get_fields_from_hit(hit: dict[str, Any]) -> dict[str, list[Any]]:
81131

82132

83133
def get_field_from_hit(hit: dict[str, Any], field: str) -> list[Any]:
134+
"""Extract a specific field value from an Elasticsearch hit.
135+
136+
Args:
137+
hit: A single hit dictionary from an Elasticsearch response.
138+
field: The name of the field to extract.
139+
140+
Returns:
141+
The field value as a list, or an empty list if the fields object is missing.
142+
143+
Raises:
144+
TypeError: If the specified field is not present in the hit.
145+
"""
84146
if not (fields := get_fields_from_hit(hit=hit)):
85147
return []
86148

@@ -92,6 +154,19 @@ def get_field_from_hit(hit: dict[str, Any], field: str) -> list[Any]:
92154

93155

94156
def get_values_from_field_in_hit(hit: dict[str, Any], field: str, value_type: type[T]) -> list[T]:
157+
"""Extract and type-check field values from an Elasticsearch hit.
158+
159+
Args:
160+
hit: A single hit dictionary from an Elasticsearch response.
161+
field: The name of the field to extract.
162+
value_type: The expected type of values in the field list.
163+
164+
Returns:
165+
A list of values of the specified type.
166+
167+
Raises:
168+
TypeError: If the field is missing or contains values of the wrong type.
169+
"""
95170
if not (value := get_field_from_hit(hit=hit, field=field)):
96171
msg = f"Field {field} is not in hit {hit}"
97172
raise TypeError(msg)
@@ -104,6 +179,19 @@ def get_values_from_field_in_hit(hit: dict[str, Any], field: str, value_type: ty
104179

105180

106181
def get_first_value_from_field_in_hit(hit: dict[str, Any], field: str, value_type: type[T]) -> T:
182+
"""Extract and validate a single-value field from an Elasticsearch hit.
183+
184+
Args:
185+
hit: A single hit dictionary from an Elasticsearch response.
186+
field: The name of the field to extract.
187+
value_type: The expected type of the value.
188+
189+
Returns:
190+
The single value from the field.
191+
192+
Raises:
193+
TypeError: If the field doesn't contain exactly one value, or if the value type is incorrect.
194+
"""
107195
values: list[T] = get_values_from_field_in_hit(hit=hit, field=field, value_type=value_type)
108196
if len(values) != 1:
109197
msg: str = f"Field {field} in hit {hit} is not a single value"
@@ -112,6 +200,19 @@ def get_first_value_from_field_in_hit(hit: dict[str, Any], field: str, value_typ
112200

113201

114202
def managed_entry_to_document(collection: str, key: str, managed_entry: ManagedEntry) -> dict[str, Any]:
203+
"""Convert a ManagedEntry to an Elasticsearch document format.
204+
205+
This function serializes a ManagedEntry to a document suitable for storage in Elasticsearch,
206+
including collection, key, and value fields, plus optional timestamp metadata.
207+
208+
Args:
209+
collection: The collection name to include in the document.
210+
key: The key to include in the document.
211+
managed_entry: The ManagedEntry to serialize.
212+
213+
Returns:
214+
An Elasticsearch document dictionary ready for indexing.
215+
"""
115216
document: dict[str, Any] = {"collection": collection, "key": key, "value": managed_entry.to_json(include_metadata=False)}
116217

117218
if managed_entry.created_at:
@@ -123,4 +224,14 @@ def managed_entry_to_document(collection: str, key: str, managed_entry: ManagedE
123224

124225

125226
def new_bulk_action(action: str, index: str, document_id: str) -> dict[str, Any]:
227+
"""Create a bulk action descriptor for Elasticsearch bulk operations.
228+
229+
Args:
230+
action: The bulk action type (e.g., "index", "delete", "update").
231+
index: The Elasticsearch index name.
232+
document_id: The document ID.
233+
234+
Returns:
235+
A bulk action dictionary formatted for Elasticsearch's bulk API.
236+
"""
126237
return {action: {"_index": index, "_id": document_id}}

0 commit comments

Comments
 (0)