daplug-ddb 1.0.0b1__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 +17 -0
- daplug_ddb/adapter.py +135 -0
- daplug_ddb/common/__init__.py +11 -0
- daplug_ddb/common/base_adapter.py +57 -0
- daplug_ddb/common/dict_merger.py +66 -0
- daplug_ddb/common/json_helper.py +19 -0
- daplug_ddb/common/logger.py +16 -0
- daplug_ddb/common/publisher.py +29 -0
- daplug_ddb/common/schema_loader.py +13 -0
- daplug_ddb/common/schema_mapper.py +51 -0
- daplug_ddb/exception.py +2 -0
- daplug_ddb/types/__init__.py +7 -0
- daplug_ddb/types/dynamo_item.py +7 -0
- daplug_ddb/types/dynamo_items.py +9 -0
- daplug_ddb/types/message_attributes.py +7 -0
- daplug_ddb-1.0.0b1.dist-info/METADATA +292 -0
- daplug_ddb-1.0.0b1.dist-info/RECORD +27 -0
- daplug_ddb-1.0.0b1.dist-info/WHEEL +5 -0
- daplug_ddb-1.0.0b1.dist-info/licenses/LICENSE +201 -0
- daplug_ddb-1.0.0b1.dist-info/top_level.txt +2 -0
- tests/__init__.py +1 -0
- tests/integration/__init__.py +0 -0
- tests/integration/mock_table.py +63 -0
- tests/integration/test_adapter.py +341 -0
- tests/unit/__init__.py +0 -0
- tests/unit/mocks.py +78 -0
- tests/unit/test_adapter_unit.py +95 -0
daplug_ddb/__init__.py
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
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 mirroring legacy entry point for DynamoDB adapters."""
|
|
10
|
+
|
|
11
|
+
engine = kwargs.pop("engine", "dynamodb")
|
|
12
|
+
if engine and engine != "dynamodb": # preserve deterministic erroring for unsupported engines
|
|
13
|
+
raise ValueError(f"engine {engine} not supported; only 'dynamodb' is available")
|
|
14
|
+
return DynamodbAdapter(**kwargs)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
__all__ = ["adapter", "DynamodbAdapter", "BatchItemException"]
|
daplug_ddb/adapter.py
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"""DynamoDB adapter exposing normalized CRUD operations."""
|
|
2
|
+
|
|
3
|
+
from functools import lru_cache
|
|
4
|
+
from typing import Any, Dict, Iterable, Optional, Union, cast
|
|
5
|
+
|
|
6
|
+
import boto3
|
|
7
|
+
from boto3.dynamodb.conditions import Attr
|
|
8
|
+
|
|
9
|
+
from daplug_ddb.types import DynamoItem, DynamoItems
|
|
10
|
+
|
|
11
|
+
from .common import map_to_schema, merge
|
|
12
|
+
from .common.base_adapter import BaseAdapter
|
|
13
|
+
from .exception import BatchItemException
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DynamodbAdapter(BaseAdapter):
|
|
17
|
+
"""Implements DynamoDB CRUD operations with schema normalization."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, **kwargs: Any) -> None:
|
|
20
|
+
super().__init__(**kwargs)
|
|
21
|
+
self.table = self._get_dynamo_table(kwargs["table"], kwargs.get("endpoint"))
|
|
22
|
+
self.schema_file: str = kwargs["schema_file"]
|
|
23
|
+
self.schema: str = kwargs["schema"]
|
|
24
|
+
self.identifier: str = kwargs["identifier"]
|
|
25
|
+
self.idempotence_key: Optional[str] = kwargs.get("idempotence_key")
|
|
26
|
+
|
|
27
|
+
@lru_cache(maxsize=128)
|
|
28
|
+
def _get_dynamo_table(self, table: str, endpoint: Optional[str] = None) -> Any:
|
|
29
|
+
return boto3.resource("dynamodb", endpoint_url=endpoint).Table(table)
|
|
30
|
+
|
|
31
|
+
def create(self, **kwargs: Any) -> DynamoItem:
|
|
32
|
+
if kwargs.get("operation") == "overwrite":
|
|
33
|
+
return self.overwrite(**kwargs)
|
|
34
|
+
return self.insert(**kwargs)
|
|
35
|
+
|
|
36
|
+
def read(self, **kwargs: Any) -> Union[DynamoItem, DynamoItems, Dict[str, Any]]:
|
|
37
|
+
if kwargs.get("operation") == "query":
|
|
38
|
+
return self.query(**kwargs)
|
|
39
|
+
if kwargs.get("operation") == "scan":
|
|
40
|
+
return self.scan(**kwargs)
|
|
41
|
+
return self.get(**kwargs)
|
|
42
|
+
|
|
43
|
+
def scan(self, **kwargs: Any) -> Union[DynamoItems, Dict[str, Any]]:
|
|
44
|
+
if kwargs.get("raw_scan"):
|
|
45
|
+
return self.table.scan(**kwargs.get("query", {}))
|
|
46
|
+
return self.table.scan(**kwargs.get("query", {})).get("Items", [])
|
|
47
|
+
|
|
48
|
+
def get(self, **kwargs: Any) -> DynamoItem:
|
|
49
|
+
result: Dict[str, Any] = self.table.get_item(**kwargs.get("query", {}))
|
|
50
|
+
return result.get("Item", {})
|
|
51
|
+
|
|
52
|
+
def query(self, **kwargs: Any) -> Union[DynamoItems, Dict[str, Any]]:
|
|
53
|
+
if kwargs.get("raw_query"):
|
|
54
|
+
return self.table.query(**kwargs.get("query", {}))
|
|
55
|
+
return self.table.query(**kwargs.get("query", {})).get("Items", [])
|
|
56
|
+
|
|
57
|
+
def overwrite(self, **kwargs: Any) -> DynamoItem:
|
|
58
|
+
overwrite_item = map_to_schema(kwargs["data"], self.schema_file, self.schema)
|
|
59
|
+
self.table.put_item(Item=overwrite_item)
|
|
60
|
+
super().publish("create", overwrite_item, **kwargs)
|
|
61
|
+
return overwrite_item
|
|
62
|
+
|
|
63
|
+
def insert(self, **kwargs: Any) -> DynamoItem:
|
|
64
|
+
new_item = map_to_schema(kwargs["data"], self.schema_file, self.schema)
|
|
65
|
+
self.table.put_item(
|
|
66
|
+
Item=new_item, ConditionExpression=Attr(self.identifier).not_exists()
|
|
67
|
+
)
|
|
68
|
+
super().publish("create", new_item, **kwargs)
|
|
69
|
+
return new_item
|
|
70
|
+
|
|
71
|
+
def batch_insert(self, **kwargs: Any) -> None:
|
|
72
|
+
data = kwargs["data"]
|
|
73
|
+
batch_size: int = kwargs.get("batch_size", 25)
|
|
74
|
+
|
|
75
|
+
if not isinstance(data, list):
|
|
76
|
+
raise BatchItemException("Batched data must be contained within a list")
|
|
77
|
+
|
|
78
|
+
batched_data: Iterable[DynamoItems] = (
|
|
79
|
+
data[pos : pos + batch_size] for pos in range(0, len(data), batch_size)
|
|
80
|
+
)
|
|
81
|
+
with self.table.batch_writer() as writer:
|
|
82
|
+
for batch in batched_data:
|
|
83
|
+
for item in batch:
|
|
84
|
+
writer.put_item(Item=item)
|
|
85
|
+
|
|
86
|
+
def delete(self, **kwargs: Any) -> DynamoItem:
|
|
87
|
+
kwargs["query"]["ReturnValues"] = "ALL_OLD"
|
|
88
|
+
result = self.table.delete_item(**kwargs["query"]).get("Attributes", {})
|
|
89
|
+
super().publish("delete", result, **kwargs)
|
|
90
|
+
return result
|
|
91
|
+
|
|
92
|
+
def batch_delete(self, **kwargs: Any) -> None:
|
|
93
|
+
batch_size: int = kwargs.get("batch_size", 25)
|
|
94
|
+
if not isinstance(kwargs["data"], list):
|
|
95
|
+
raise BatchItemException("Batched data must be contained within a list")
|
|
96
|
+
batched_data: Iterable[DynamoItems] = (
|
|
97
|
+
kwargs["data"][pos : pos + batch_size]
|
|
98
|
+
for pos in range(0, len(kwargs["data"]), batch_size)
|
|
99
|
+
)
|
|
100
|
+
with self.table.batch_writer() as writer:
|
|
101
|
+
for batch in batched_data:
|
|
102
|
+
for item in batch:
|
|
103
|
+
writer.delete_item(Key=item)
|
|
104
|
+
|
|
105
|
+
def update(self, **kwargs: Any) -> DynamoItem:
|
|
106
|
+
original_data = self._get_original_data(**kwargs)
|
|
107
|
+
merged_data = merge(original_data, kwargs["data"], **kwargs)
|
|
108
|
+
updated_data = map_to_schema(merged_data, self.schema_file, self.schema)
|
|
109
|
+
put_kwargs = {"Item": updated_data}
|
|
110
|
+
if self.idempotence_key:
|
|
111
|
+
original_value = original_data.get(self.idempotence_key)
|
|
112
|
+
if original_value is None:
|
|
113
|
+
raise ValueError(
|
|
114
|
+
f"idempotence key '{self.idempotence_key}' not found in original item"
|
|
115
|
+
)
|
|
116
|
+
put_kwargs["ConditionExpression"] = cast(Any, Attr(self.idempotence_key).eq(original_value))
|
|
117
|
+
self.table.put_item(**put_kwargs)
|
|
118
|
+
super().publish("update", updated_data, **kwargs)
|
|
119
|
+
return updated_data
|
|
120
|
+
|
|
121
|
+
def _get_original_data(self, **kwargs: Any) -> DynamoItem:
|
|
122
|
+
if kwargs["operation"] == "get":
|
|
123
|
+
original_data = self.get(**kwargs)
|
|
124
|
+
else:
|
|
125
|
+
query_result = self.query(**kwargs)
|
|
126
|
+
if isinstance(query_result, list):
|
|
127
|
+
items = query_result
|
|
128
|
+
else:
|
|
129
|
+
items = cast(DynamoItems, query_result.get("Items", []))
|
|
130
|
+
if not items:
|
|
131
|
+
raise ValueError("update: no data found to update")
|
|
132
|
+
original_data = items[0]
|
|
133
|
+
if not original_data:
|
|
134
|
+
raise ValueError("update: no data found to update")
|
|
135
|
+
return original_data
|
|
@@ -0,0 +1,57 @@
|
|
|
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
|
+
"schema": kwargs.get("schema"),
|
|
21
|
+
"identifier": kwargs.get("identifier"),
|
|
22
|
+
"idempotence_key": kwargs.get("idempotence_key"),
|
|
23
|
+
"author_identifier": kwargs.get("author_identifier"),
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
def publish(self, db_operation: str, db_data: Dict[str, Any], **kwargs: Any) -> None:
|
|
27
|
+
attributes = self.create_format_attibutes(db_operation)
|
|
28
|
+
self.publisher.publish(
|
|
29
|
+
endpoint=self.sns_endpoint,
|
|
30
|
+
arn=self.sns_arn,
|
|
31
|
+
attributes=attributes,
|
|
32
|
+
data=db_data,
|
|
33
|
+
fifo_group_id=kwargs.get("fifo_group_id"),
|
|
34
|
+
fifo_duplication_id=kwargs.get("fifo_duplication_id"),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
def create_format_attibutes(self, operation: str) -> MessageAttributes:
|
|
38
|
+
self.default_attributes["operation"] = operation
|
|
39
|
+
custom_attributes = self.get_attributes()
|
|
40
|
+
formatted_attributes: MessageAttributes = {}
|
|
41
|
+
for key, value in custom_attributes.items():
|
|
42
|
+
if value is not None:
|
|
43
|
+
data_type = "String" if isinstance(value, str) else "Number"
|
|
44
|
+
formatted_attributes[key] = {
|
|
45
|
+
"DataType": data_type,
|
|
46
|
+
"StringValue": value,
|
|
47
|
+
}
|
|
48
|
+
return formatted_attributes
|
|
49
|
+
|
|
50
|
+
def get_attributes(self) -> Dict[str, Any]:
|
|
51
|
+
if self.sns_defaults and self.sns_custom:
|
|
52
|
+
return {**self.default_attributes, **self.sns_custom}
|
|
53
|
+
if not self.sns_defaults and self.sns_custom:
|
|
54
|
+
return self.sns_custom
|
|
55
|
+
if self.sns_defaults and not self.sns_custom:
|
|
56
|
+
return self.default_attributes
|
|
57
|
+
return {}
|
|
@@ -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)
|
daplug_ddb/exception.py
ADDED