daplug-ddb 1.0.0b8__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
daplug_ddb/__init__.py ADDED
@@ -0,0 +1,15 @@
1
+ """Public interface for the daplug_ddb package."""
2
+
3
+ from typing import Any
4
+
5
+ from .adapter import BatchItemException, DynamodbAdapter
6
+
7
+
8
+ def adapter(**kwargs: Any) -> DynamodbAdapter:
9
+ """Factory helper for creating a DynamoDB adapter."""
10
+
11
+ kwargs.pop("engine", None) # allow legacy callers to pass engine without effect
12
+ return DynamodbAdapter(**kwargs)
13
+
14
+
15
+ __all__ = ["adapter", "DynamodbAdapter", "BatchItemException"]
daplug_ddb/adapter.py ADDED
@@ -0,0 +1,303 @@
1
+ """DynamoDB adapter exposing normalized CRUD operations."""
2
+
3
+ from copy import deepcopy
4
+ from datetime import datetime
5
+ from functools import lru_cache
6
+ from typing import Any, Dict, Iterable, Optional, Union
7
+
8
+ import boto3
9
+ from boto3.dynamodb.conditions import Attr
10
+
11
+ from daplug_ddb.prefixer import DynamodbPrefixer
12
+ from daplug_ddb.types import DynamoItem, DynamoItems
13
+
14
+ from .common import map_to_schema, merge
15
+ from .common.base_adapter import BaseAdapter
16
+ from .exception import BatchItemException
17
+
18
+
19
+ class DynamodbAdapter(BaseAdapter):
20
+ """Implements DynamoDB CRUD operations with schema normalization."""
21
+
22
+ def __init__(self, **kwargs: Any) -> None:
23
+ super().__init__(**kwargs)
24
+ self.table = self.__get_dynamo_table(kwargs["table"], kwargs.get("endpoint"))
25
+ self.schema_file: Optional[str] = kwargs.get("schema_file")
26
+ self.identifier: str = kwargs.get("identifier", "")
27
+ self.idempotence_key: Optional[str] = kwargs.get("idempotence_key")
28
+ self.raise_idempotence_error: bool = kwargs.get("raise_idempotence_error", False)
29
+ self.idempotence_use_latest: bool = kwargs.get("idempotence_use_latest", False)
30
+ self._prefix_keys = ("hash_key", "hash_prefix", "range_key", "range_prefix")
31
+ self._default_prefix_config = self.__extract_prefix_config(kwargs)
32
+
33
+ @property
34
+ def prefixing_enabled(self) -> bool:
35
+ """Returns True when adapter-level prefix config has been supplied."""
36
+
37
+ return bool(self._default_prefix_config)
38
+
39
+ def create(self, **kwargs: Any) -> DynamoItem:
40
+ if kwargs.get("operation") == "overwrite":
41
+ return self.overwrite(**kwargs)
42
+ return self.insert(**kwargs)
43
+
44
+ def read(self, **kwargs: Any) -> Union[DynamoItem, DynamoItems, Dict[str, Any]]:
45
+ if kwargs.get("operation") == "query":
46
+ return self.query(**kwargs)
47
+ if kwargs.get("operation") == "scan":
48
+ return self.scan(**kwargs)
49
+ return self.get(**kwargs)
50
+
51
+ def scan(self, **kwargs: Any) -> Union[DynamoItems, Dict[str, Any]]:
52
+ prefixer = self.__build_prefixer(kwargs)
53
+ request_args = self.__prepare_request_arguments(prefixer, kwargs)
54
+ response = self.table.scan(**request_args)
55
+ if not prefixer.enabled:
56
+ return response if kwargs.get("raw_scan") else response.get("Items", [])
57
+ cleaned_response = prefixer.apply_response(response, add=False)
58
+ if kwargs.get("raw_scan"):
59
+ return cleaned_response if cleaned_response is not None else response
60
+ cleaned = cleaned_response or {}
61
+ return cleaned.get("Items", [])
62
+
63
+ def get(self, **kwargs: Any) -> DynamoItem:
64
+ prefixer = self.__build_prefixer(kwargs)
65
+ request_args = self.__prepare_request_arguments(prefixer, kwargs)
66
+ result: Dict[str, Any] = self.table.get_item(**request_args)
67
+ item = result.get("Item", {})
68
+ if not prefixer.enabled:
69
+ return item if isinstance(item, dict) else {}
70
+ cleaned = prefixer.apply_item(item, add=False)
71
+ return cleaned if isinstance(cleaned, dict) else item
72
+
73
+ def query(self, **kwargs: Any) -> Union[DynamoItems, Dict[str, Any]]:
74
+ prefixer = self.__build_prefixer(kwargs)
75
+ request_args = self.__prepare_request_arguments(prefixer, kwargs)
76
+ response = self.table.query(**request_args)
77
+ if not prefixer.enabled:
78
+ return response if kwargs.get("raw_query") else response.get("Items", [])
79
+ cleaned_response = prefixer.apply_response(response, add=False)
80
+ if kwargs.get("raw_query"):
81
+ return cleaned_response if cleaned_response is not None else response
82
+ cleaned = cleaned_response or {}
83
+ return cleaned.get("Items", [])
84
+
85
+ def overwrite(self, **kwargs: Any) -> DynamoItem:
86
+ payload = self.__map_with_schema(kwargs["data"], kwargs)
87
+ prefixer = self.__build_prefixer(kwargs)
88
+ item_to_store = (
89
+ prefixer.apply_item(payload, add=True)
90
+ if prefixer.enabled
91
+ else payload
92
+ )
93
+ self.table.put_item(Item=item_to_store)
94
+ response_item = (
95
+ prefixer.apply_item(item_to_store, add=False)
96
+ if prefixer.enabled
97
+ else payload
98
+ )
99
+ result_item = response_item if isinstance(response_item, dict) else payload
100
+ super().publish("create", result_item, **kwargs)
101
+ return result_item
102
+
103
+ def insert(self, **kwargs: Any) -> DynamoItem:
104
+ payload = self.__map_with_schema(kwargs["data"], kwargs)
105
+ prefixer = self.__build_prefixer(kwargs)
106
+ item_to_store = (
107
+ prefixer.apply_item(payload, add=True)
108
+ if prefixer.enabled
109
+ else payload
110
+ )
111
+ self.table.put_item(
112
+ Item=item_to_store,
113
+ ConditionExpression=Attr(self.identifier).not_exists(),
114
+ )
115
+ response_item = (
116
+ prefixer.apply_item(item_to_store, add=False)
117
+ if prefixer.enabled
118
+ else payload
119
+ )
120
+ result_item = response_item if isinstance(response_item, dict) else payload
121
+ super().publish("create", result_item, **kwargs)
122
+ return result_item
123
+
124
+ def batch_insert(self, **kwargs: Any) -> None:
125
+ data = kwargs["data"]
126
+ batch_size: int = kwargs.get("batch_size", 25)
127
+
128
+ if not isinstance(data, list):
129
+ raise BatchItemException("Batched data must be contained within a list")
130
+ mapped_items = [self.__map_with_schema(item, kwargs) for item in data]
131
+ prefixer = self.__build_prefixer(kwargs)
132
+ batched_data: Iterable[DynamoItems] = (
133
+ mapped_items[pos: pos + batch_size] for pos in range(0, len(mapped_items), batch_size)
134
+ )
135
+ with self.table.batch_writer() as writer:
136
+ for batch in batched_data:
137
+ if prefixer.enabled:
138
+ prefixed_batch = prefixer.apply_items(batch, add=True)
139
+ items_to_store = prefixed_batch if prefixed_batch is not None else batch
140
+ else:
141
+ items_to_store = batch
142
+ for item in items_to_store:
143
+ writer.put_item(Item=item)
144
+
145
+ def delete(self, **kwargs: Any) -> DynamoItem:
146
+ prefixer = self.__build_prefixer(kwargs)
147
+ request_args = self.__prepare_request_arguments(prefixer, kwargs)
148
+ request_args["ReturnValues"] = "ALL_OLD"
149
+ result = self.table.delete_item(**request_args).get("Attributes", {})
150
+ if prefixer.enabled:
151
+ cleaned = prefixer.apply_item(result, add=False)
152
+ cleaned_item = cleaned if isinstance(cleaned, dict) else result
153
+ else:
154
+ cleaned_item = result
155
+ super().publish("delete", cleaned_item, **kwargs)
156
+ return cleaned_item if isinstance(cleaned_item, dict) else {}
157
+
158
+ def batch_delete(self, **kwargs: Any) -> None:
159
+ batch_size: int = kwargs.get("batch_size", 25)
160
+ if not isinstance(kwargs["data"], list):
161
+ raise BatchItemException("Batched data must be contained within a list")
162
+ batched_data: Iterable[DynamoItems] = (
163
+ kwargs["data"][pos: pos + batch_size]
164
+ for pos in range(0, len(kwargs["data"]), batch_size)
165
+ )
166
+ prefixer = self.__build_prefixer(kwargs)
167
+ with self.table.batch_writer() as writer:
168
+ for batch in batched_data:
169
+ if prefixer.enabled:
170
+ prefixed_batch = prefixer.apply_items(batch, add=True)
171
+ items_to_delete = prefixed_batch if prefixed_batch is not None else batch
172
+ else:
173
+ items_to_delete = batch
174
+ for item in items_to_delete:
175
+ writer.delete_item(Key=item)
176
+
177
+ def update(self, **kwargs: Any) -> DynamoItem:
178
+ prefixer = self.__build_prefixer(kwargs)
179
+ original_data = self.__get_original_data(**kwargs)
180
+ merged_data = merge(original_data, kwargs["data"], **kwargs)
181
+ payload = self.__map_with_schema(merged_data, kwargs)
182
+ if prefixer.enabled:
183
+ prefixed_item = prefixer.apply_item(payload, add=True)
184
+ data_to_store = prefixed_item if isinstance(prefixed_item, dict) else payload
185
+ response_template_raw = prefixer.apply_item(data_to_store, add=False)
186
+ response_template = (
187
+ response_template_raw if isinstance(response_template_raw, dict) else payload
188
+ )
189
+ else:
190
+ data_to_store = payload
191
+ response_template = payload
192
+ key_name = self.idempotence_key
193
+ original_value = (
194
+ original_data.get(key_name)
195
+ if isinstance(original_data, dict) and key_name
196
+ else None
197
+ )
198
+ new_value = (
199
+ response_template.get(key_name)
200
+ if isinstance(response_template, dict) and key_name
201
+ else None
202
+ )
203
+ if self.__should_use_latest(original_value, new_value):
204
+ return self.__clean_for_response(prefixer, original_data)
205
+ put_kwargs = self.__build_put_kwargs(original_value, data_to_store)
206
+ self.table.put_item(**put_kwargs)
207
+ cleaned_item = self.__clean_for_response(prefixer, data_to_store)
208
+ super().publish("update", cleaned_item, **kwargs)
209
+ return cleaned_item
210
+
211
+ @lru_cache(maxsize=128)
212
+ def __get_dynamo_table(self, table: str, endpoint: Optional[str] = None) -> Any:
213
+ return boto3.resource("dynamodb", endpoint_url=endpoint).Table(table)
214
+
215
+ def __build_put_kwargs(self, original_value: Any, data_to_store: Dict[str, Any]) -> Dict[str, Any]:
216
+ put_kwargs: Dict[str, Any] = {"Item": data_to_store}
217
+ if not self.idempotence_key:
218
+ return put_kwargs
219
+ if original_value is None:
220
+ if self.raise_idempotence_error:
221
+ raise ValueError(
222
+ f"idempotence key '{self.idempotence_key}' not found in original item"
223
+ )
224
+ return put_kwargs
225
+ if (
226
+ original_value != data_to_store.get(self.idempotence_key)
227
+ and self.raise_idempotence_error
228
+ ):
229
+ raise ValueError("update: idempotence key value has changed")
230
+ put_kwargs["ConditionExpression"] = Attr(self.idempotence_key).eq(original_value)
231
+ return put_kwargs
232
+
233
+ def __clean_for_response(self, prefixer: DynamodbPrefixer, item: Dict[str, Any]) -> Dict[str, Any]:
234
+ if not prefixer.enabled:
235
+ return item
236
+ cleaned = prefixer.apply_item(item, add=False)
237
+ return cleaned if isinstance(cleaned, dict) else item
238
+
239
+ def __get_original_data(self, **kwargs: Any) -> DynamoItem:
240
+ if kwargs["operation"] == "get":
241
+ original_data = self.get(**kwargs)
242
+ else:
243
+ query_result = self.query(**kwargs)
244
+ if isinstance(query_result, list):
245
+ items = query_result
246
+ elif isinstance(query_result, dict):
247
+ items = query_result.get("Items", [])
248
+ else:
249
+ items = []
250
+ if not items:
251
+ raise ValueError("update: no data found to update")
252
+ original_data = items[0]
253
+ if not original_data:
254
+ raise ValueError("update: no data found to update")
255
+ return original_data
256
+
257
+ def __should_use_latest(self, original_value: Any, new_value: Any) -> bool:
258
+ if not self.idempotence_use_latest or not self.idempotence_key:
259
+ return False
260
+ if original_value is None or new_value is None:
261
+ return False
262
+ try:
263
+ original_dt = datetime.fromisoformat(str(original_value))
264
+ new_dt = datetime.fromisoformat(str(new_value))
265
+ except ValueError as exc:
266
+ raise ValueError("idempotence_use_latest requires ISO date-compatible values") from exc
267
+ return original_dt > new_dt
268
+
269
+ def __map_with_schema(self, data: Dict[str, Any], call_kwargs: Dict[str, Any]) -> Dict[str, Any]:
270
+ schema_file = call_kwargs.get("schema_file") or self.schema_file
271
+ schema_name = call_kwargs.get("schema")
272
+ if schema_file and schema_name:
273
+ return map_to_schema(data, schema_file, schema_name)
274
+ return deepcopy(data)
275
+
276
+ def __build_prefixer(self, call_kwargs: Dict[str, Any]) -> DynamodbPrefixer:
277
+ config = dict(self._default_prefix_config)
278
+ for key in self._prefix_keys:
279
+ value = call_kwargs.get(key)
280
+ if value is not None:
281
+ config[key] = value
282
+ return DynamodbPrefixer(**config)
283
+
284
+ def __extract_prefix_config(self, source: Dict[str, Any]) -> Dict[str, Any]:
285
+ config: Dict[str, Any] = {}
286
+ for key in ("hash_key", "hash_prefix", "range_key", "range_prefix"):
287
+ value = source.get(key)
288
+ if value is not None:
289
+ config[key] = value
290
+ return config
291
+
292
+ def __prepare_request_arguments(
293
+ self,
294
+ prefixer: DynamodbPrefixer,
295
+ call_kwargs: Dict[str, Any],
296
+ ) -> Dict[str, Any]:
297
+ raw_query = call_kwargs.get("query") or {}
298
+ if not isinstance(raw_query, dict):
299
+ return {}
300
+ if prefixer.enabled:
301
+ transformed = prefixer.apply_request(raw_query, add=True)
302
+ return transformed if isinstance(transformed, dict) else {}
303
+ return deepcopy(raw_query)
@@ -0,0 +1,11 @@
1
+ """Shared utilities used by the DynamoDB adapter."""
2
+
3
+ from .base_adapter import BaseAdapter
4
+ from .dict_merger import merge
5
+ from .schema_mapper import map_to_schema
6
+
7
+ __all__ = [
8
+ "BaseAdapter",
9
+ "map_to_schema",
10
+ "merge",
11
+ ]
@@ -0,0 +1,69 @@
1
+ """Shared base adapter that wraps SNS publishing."""
2
+
3
+ from typing import Any, Dict, Optional
4
+
5
+ from daplug_ddb.types import MessageAttributes
6
+
7
+ from . import publisher
8
+
9
+
10
+ class BaseAdapter:
11
+ """Provides shared publish helper logic for adapters."""
12
+
13
+ def __init__(self, **kwargs: Any) -> None:
14
+ self.sns_arn: Optional[str] = kwargs.get("sns_arn")
15
+ self.sns_custom: Dict[str, Any] = kwargs.get("sns_attributes", {})
16
+ self.sns_defaults: bool = kwargs.get("sns_default_attributes", True)
17
+ self.sns_endpoint: Optional[str] = kwargs.get("sns_endpoint")
18
+ self.publisher = publisher
19
+ self.default_attributes: Dict[str, Any] = {
20
+ "identifier": kwargs.get("identifier"),
21
+ "idempotence_key": kwargs.get("idempotence_key"),
22
+ "author_identifier": kwargs.get("author_identifier"),
23
+ }
24
+
25
+ def publish(self, db_operation: str, db_data: Dict[str, Any], **kwargs: Any) -> None:
26
+ attributes = self.create_format_attibutes(db_operation, kwargs.get("sns_attributes", {}))
27
+ self.publisher.publish(
28
+ endpoint=self.sns_endpoint,
29
+ arn=self.sns_arn,
30
+ attributes=attributes,
31
+ data=db_data,
32
+ fifo_group_id=kwargs.get("fifo_group_id"),
33
+ fifo_duplication_id=kwargs.get("fifo_duplication_id"),
34
+ )
35
+
36
+ def create_format_attibutes(self, operation: str, call_attributes: dict) -> MessageAttributes:
37
+ combined = self.__combined_attributes(operation, call_attributes)
38
+ formatted_attributes: MessageAttributes = {}
39
+ for key, value in combined.items():
40
+ if value is not None:
41
+ data_type = "String" if isinstance(value, str) else "Number"
42
+ formatted_attributes[key] = {
43
+ "DataType": data_type,
44
+ "StringValue": value,
45
+ }
46
+ return formatted_attributes
47
+
48
+ def __combined_attributes(self, operation: str, call_attributes: dict) -> Dict[str, Any]:
49
+ pieces = []
50
+ base: Dict[str, Any] = {}
51
+ if self.sns_defaults:
52
+ base.update(self.default_attributes)
53
+ base["operation"] = operation
54
+ pieces.append(base)
55
+
56
+ if self.sns_custom:
57
+ pieces.append(self.sns_custom)
58
+ if call_attributes:
59
+ pieces.append(call_attributes)
60
+
61
+ return self.__merge_attributes(*pieces)
62
+
63
+ def __merge_attributes(self, *dicts: Dict[str, Any]) -> Dict[str, Any]:
64
+ merged: Dict[str, Any] = {}
65
+ for data in dicts:
66
+ if not data:
67
+ continue
68
+ merged.update(data)
69
+ return merged
@@ -0,0 +1,66 @@
1
+ """Utilities to merge dictionaries and lists with optional strategies."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import copy
6
+ from typing import Any, List
7
+
8
+ import simplejson as json
9
+
10
+ from daplug_ddb.types import DynamoItem
11
+
12
+
13
+ def merge(original_data: DynamoItem, new_data: DynamoItem, **kwargs: Any) -> DynamoItem:
14
+ updated_data = copy.deepcopy(original_data)
15
+ _walk_dict(updated_data, new_data, **kwargs)
16
+ return updated_data
17
+
18
+
19
+ def _walk_dict(old_data: DynamoItem, new_data: DynamoItem, **kwargs: Any) -> None:
20
+ for new_key in new_data.keys():
21
+ if old_data.get(new_key) and isinstance(new_data[new_key], dict):
22
+ _walk_dict(old_data[new_key], new_data[new_key], **kwargs)
23
+ elif isinstance(old_data.get(new_key), list) and isinstance(new_data[new_key], list):
24
+ old_data[new_key] = _merge_lists(
25
+ old_data[new_key], new_data[new_key], kwargs.get("update_list_operation", "add")
26
+ )
27
+ else:
28
+ _merge_dicts(new_key, old_data, new_data, kwargs.get("update_dict_operation", "upsert"))
29
+
30
+
31
+ def _merge_dicts(
32
+ dict_key: str,
33
+ old_dict: DynamoItem,
34
+ new_dict: DynamoItem,
35
+ update_dict_operation: str = "upsert",
36
+ ) -> None:
37
+ if update_dict_operation == "remove":
38
+ old_dict.pop(dict_key, None)
39
+ else:
40
+ old_dict[dict_key] = new_dict[dict_key]
41
+
42
+
43
+ def _merge_lists(
44
+ old_list: List[Any], new_list: List[Any], update_list_operation: str = "add"
45
+ ) -> List[Any]:
46
+ if update_list_operation == "remove":
47
+ _remove_item_in_list(old_list, new_list)
48
+ elif update_list_operation == "add":
49
+ _add_unique_item_in_list(old_list, new_list)
50
+ elif update_list_operation == "replace":
51
+ old_list = new_list
52
+ return old_list
53
+
54
+
55
+ def _remove_item_in_list(old_list: List[Any], new_list: List[Any]) -> None:
56
+ for old_item in old_list:
57
+ old = sorted(old_item.items()) if isinstance(old_item, dict) else old_item
58
+ new = sorted(new_list[0].items()) if isinstance(new_list[0], dict) else new_list[0]
59
+ if json.dumps(old) == json.dumps(new):
60
+ old_list.remove(old_item)
61
+
62
+
63
+ def _add_unique_item_in_list(old_list: List[Any], new_list: List[Any]) -> None:
64
+ for item in new_list:
65
+ if item not in old_list:
66
+ old_list.append(item)
@@ -0,0 +1,19 @@
1
+ """Safe JSON encoding/decoding helpers."""
2
+
3
+ from typing import Any
4
+
5
+ import json
6
+
7
+
8
+ def try_decode_json(possible_json: Any) -> Any:
9
+ try:
10
+ return json.loads(possible_json)
11
+ except Exception: # pylint: disable=broad-except
12
+ return possible_json
13
+
14
+
15
+ def try_encode_json(possible_json: Any) -> Any:
16
+ try:
17
+ return json.dumps(possible_json)
18
+ except Exception: # pylint: disable=broad-except
19
+ return possible_json
@@ -0,0 +1,16 @@
1
+ """Lightweight stdout logger honoring RUN_MODE."""
2
+
3
+ from typing import Any, Dict
4
+
5
+ import os
6
+
7
+ from . import json_helper
8
+
9
+
10
+ def log(**kwargs: Any) -> None:
11
+ if os.getenv("RUN_MODE") != "unittest":
12
+ payload: Dict[str, Any] = {
13
+ "level": kwargs.get("level", "INFO"),
14
+ "log": kwargs.get("log", {}),
15
+ }
16
+ print(json_helper.try_encode_json(payload))
@@ -0,0 +1,29 @@
1
+ """SNS publisher helper used by adapters for event fan-out."""
2
+
3
+ from typing import Any, Dict
4
+
5
+ import boto3
6
+ import simplejson as json
7
+
8
+ from . import logger
9
+
10
+
11
+ def publish(**kwargs: Any) -> None:
12
+ if not kwargs.get("arn") or not kwargs.get("data"):
13
+ return
14
+ try:
15
+ sns_client = boto3.client(
16
+ "sns", region_name=kwargs.get("region"), endpoint_url=kwargs.get("endpoint")
17
+ )
18
+ publish_kwargs: Dict[str, Any] = {
19
+ "TopicArn": kwargs["arn"],
20
+ "Message": json.dumps(kwargs["data"]),
21
+ "MessageAttributes": kwargs.get("attributes", {}),
22
+ }
23
+ if kwargs.get("fifo_group_id"):
24
+ publish_kwargs["MessageGroupId"] = kwargs["fifo_group_id"]
25
+ if kwargs.get("fifo_duplication_id"):
26
+ publish_kwargs["MessageDeduplicationId"] = kwargs["fifo_duplication_id"]
27
+ sns_client.publish(**publish_kwargs)
28
+ except Exception as exc: # pylint: disable=broad-except
29
+ logger.log(level="WARN", log={"error": f"publish_sns_error: {exc}"})
@@ -0,0 +1,13 @@
1
+ """Loads schema definitions with JSON reference resolution."""
2
+
3
+ from typing import Any, Dict
4
+
5
+ import jsonref
6
+ import simplejson as json
7
+ import yaml
8
+
9
+
10
+ def load_schema(schema_file: str, schema_key: str) -> Dict[str, Any]:
11
+ with open(schema_file, encoding="UTF-8") as openapi:
12
+ api_doc = yaml.load(openapi, Loader=yaml.FullLoader)
13
+ return jsonref.loads(json.dumps(api_doc))["components"]["schemas"][schema_key]
@@ -0,0 +1,51 @@
1
+ """Helpers that map arbitrary data to JSON schema-defined structures."""
2
+
3
+ from typing import Any, Dict, List
4
+
5
+ from daplug_ddb.types import DynamoItem
6
+
7
+ from . import schema_loader
8
+
9
+
10
+ def map_to_schema(data: DynamoItem, schema_file: str, schema_key: str) -> DynamoItem:
11
+ model_data: DynamoItem = {}
12
+ model_schema = schema_loader.load_schema(schema_file, schema_key)
13
+ schemas = model_schema["allOf"] if model_schema.get("allOf") else [model_schema]
14
+ for model in schemas:
15
+ if model.get("type") == "object":
16
+ _populate_model_data(model.get("properties", {}), data, model_data)
17
+ return model_data
18
+
19
+
20
+ def _populate_model_data(
21
+ properties: Dict[str, Any], data: Any, model_data: DynamoItem
22
+ ) -> DynamoItem:
23
+ if data and isinstance(data, dict):
24
+ _populate_model_dict(properties, data, model_data)
25
+ return model_data
26
+
27
+
28
+ def _populate_model_dict(properties: Dict[str, Any], data: Dict[str, Any], model_data: DynamoItem) -> None:
29
+ for property_key, property_value in properties.items():
30
+ model_data[property_key] = {}
31
+ if property_value.get("properties"):
32
+ _populate_model_data(
33
+ property_value["properties"], data.get(property_key), model_data[property_key]
34
+ )
35
+ elif property_value.get("items", {}).get("properties"):
36
+ _populate_model_list(model_data, property_key, property_value, data)
37
+ else:
38
+ model_data[property_key] = data.get(property_key)
39
+
40
+
41
+ def _populate_model_list(
42
+ model_data: DynamoItem, property_key: str, property_value: Dict[str, Any], data: Dict[str, Any]
43
+ ) -> None:
44
+ model_data[property_key] = []
45
+ items: List[Any] = data.get(property_key, [])
46
+ for index in range(len(items)): # pylint: disable=consider-using-enumerate
47
+ if data.get(property_key) and isinstance(items, list) and index < len(items):
48
+ pop = _populate_model_data(
49
+ property_value["items"]["properties"], items[index], {}
50
+ )
51
+ model_data[property_key].append(pop)
@@ -0,0 +1,2 @@
1
+ class BatchItemException(Exception):
2
+ """Raised when batched operations receive invalid input."""