daplug-cypher 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.
@@ -0,0 +1,13 @@
1
+ """Public interface for the daplug_cypher package."""
2
+
3
+ from typing import Any
4
+
5
+ from .adapter import CypherAdapter
6
+
7
+
8
+ def adapter(**kwargs: Any) -> CypherAdapter:
9
+ """Factory helper returning a configured CypherAdapter instance."""
10
+ return CypherAdapter(**kwargs)
11
+
12
+
13
+ __all__ = ["adapter", "CypherAdapter"]
@@ -0,0 +1,351 @@
1
+ """Cypher adapter implementing graph CRUD operations with Trellis-style patterns."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, Dict, List, Optional
6
+
7
+ from neo4j import GraphDatabase
8
+ from neo4j import Driver, Session, Transaction
9
+
10
+ from daplug_cypher.common import BaseAdapter, map_to_schema, merge
11
+ from daplug_cypher.cypher.parameters import convert_placeholders
12
+ from daplug_cypher.cypher.serialization import serialize_records
13
+
14
+
15
+ class CypherAdapter(BaseAdapter):
16
+ """Graph adapter coordinating schema normalization, sessions, and publishing."""
17
+
18
+ def __init__(self, **kwargs: Any) -> None:
19
+ super().__init__(**kwargs)
20
+ self.auto_connect: bool = kwargs.get("auto_connect", True)
21
+ self.bolt: Dict[str, Any] = kwargs.get("bolt", {})
22
+ self.neptune: Optional[Dict[str, Any]] = kwargs.get("neptune")
23
+ self.schema_file: Optional[str] = kwargs.get("schema_file")
24
+ self.schema_name: Optional[str] = kwargs.get("schema")
25
+ self.validate_schema: bool = kwargs.get("validate_schema", True)
26
+ self.driver_config: Dict[str, Any] = kwargs.get("driver_config", {})
27
+
28
+ self._driver: Optional[Driver] = None
29
+ self._session: Optional[Session] = None
30
+
31
+ # ---- Connection lifecycle -------------------------------------------------
32
+ def open(self) -> None:
33
+ """Establish a Cypher session using Neo4j driver configuration."""
34
+ if self._session:
35
+ return
36
+
37
+ bolt_config = self.__resolve_bolt_config()
38
+ uri = bolt_config.get("url")
39
+ user = bolt_config.get("user")
40
+ password = bolt_config.get("password")
41
+
42
+ if not uri or not user:
43
+ raise ValueError("bolt configuration requires 'url' and 'user'")
44
+
45
+ auth = (user, password) if password is not None else None
46
+ driver: Driver = GraphDatabase.driver(uri, auth=auth, **self.driver_config)
47
+ session: Session = driver.session()
48
+ self._driver = driver
49
+ self._session = session
50
+
51
+ def close(self) -> None:
52
+ """Close any active driver/session."""
53
+ if self._session:
54
+ self._session.close()
55
+ self._session = None
56
+ if self._driver:
57
+ self._driver.close()
58
+ self._driver = None
59
+
60
+ def __auto_open(self) -> None:
61
+ """Open a session when auto-connect is enabled."""
62
+ if self.auto_connect:
63
+ self.open()
64
+
65
+ def __auto_close(self) -> None:
66
+ """Close a session when auto-connect is enabled."""
67
+ if self.auto_connect:
68
+ self.close()
69
+
70
+ # ---- CRUD surface ---------------------------------------------------------
71
+ def create(self, **kwargs: Any) -> Dict[str, Any]:
72
+ """Insert a node using schema-normalized payloads."""
73
+ node_label = kwargs.get("node") or kwargs.get("label")
74
+ if not node_label:
75
+ raise ValueError("node label must be provided for create operations")
76
+ payload = self.__map_with_schema(kwargs["data"])
77
+ query = kwargs.get("query") or self.__default_create_query(node_label)
78
+
79
+ def _create(tx: Transaction) -> Any:
80
+ result = tx.run(query, placeholder=payload)
81
+ result.consume()
82
+
83
+ self.__auto_open()
84
+ try:
85
+ self.__execute_write(_create)
86
+ finally:
87
+ self.__auto_close()
88
+
89
+ self.publish("create", payload, **kwargs)
90
+ return payload
91
+
92
+ def read(self, **kwargs: Any) -> Any:
93
+ """Dispatch to query/match readers and return serialized content."""
94
+ params = dict(kwargs)
95
+ serialize = params.pop("serialize", True)
96
+ search = params.pop("search", False)
97
+ node_label = params.get("node") or params.get("label")
98
+ query = params["query"]
99
+ placeholder = params.get("placeholder")
100
+ records = self.__match(query, placeholder, node_label=node_label, serialize=serialize, search=search)
101
+ return records
102
+
103
+ def query(self, **kwargs: Any) -> Any:
104
+ """Execute arbitrary parameterized queries."""
105
+ if "query" not in kwargs:
106
+ raise ValueError("query text is required")
107
+ query_text = kwargs["query"]
108
+ if "$" not in query_text:
109
+ raise ValueError("SECURITY ERROR: parameter placeholders ($) are required")
110
+
111
+ parameters = self.__clean_placeholders(kwargs.get("placeholder") or {})
112
+
113
+ self.__auto_open()
114
+ try:
115
+ result = self.__run_read(query_text, parameters)
116
+ return list(result)
117
+ finally:
118
+ self.__auto_close()
119
+
120
+ def update(self, **kwargs: Any) -> Dict[str, Any]:
121
+ """Perform optimistic updates leveraging merge + schema mapping."""
122
+ node_label = kwargs.get("node") or kwargs.get("label")
123
+ if not node_label:
124
+ raise ValueError("node label must be provided for update operations")
125
+ identifier = kwargs.get("identifier")
126
+ idempotence_key = kwargs.get("idempotence_key")
127
+ if not identifier or not idempotence_key:
128
+ raise ValueError("identifier and idempotence_key must be provided for updates")
129
+ original_version = kwargs.get("original_idempotence_value")
130
+ if original_version is None:
131
+ raise ValueError("original_idempotence_value is required for optimistic updates")
132
+
133
+ query_text = kwargs.get("query")
134
+ if not query_text:
135
+ raise ValueError("query text is required for update operations")
136
+ placeholder = kwargs.get("placeholder")
137
+
138
+ original_records: List[Any] = self.__match(
139
+ query_text,
140
+ placeholder,
141
+ node_label=node_label,
142
+ serialize=False,
143
+ search=kwargs.get("search", False),
144
+ )
145
+ if not original_records:
146
+ raise ValueError("ATOMIC ERROR: No records found; record may have been deleted")
147
+
148
+ original_node = self.__first_node(original_records[0])
149
+ if original_node is None:
150
+ raise ValueError("ATOMIC ERROR: Unable to read existing node properties")
151
+
152
+ original_properties = dict(original_node)
153
+ merged = self.__merge_payload(original_properties, kwargs["data"], **kwargs)
154
+ normalized = self.__map_with_schema(merged)
155
+
156
+ update_query = kwargs.get("update_query") or self.__default_update_query(node_label, identifier, idempotence_key)
157
+ update_params = {
158
+ "id": normalized[identifier],
159
+ "version": original_version,
160
+ "placeholder": normalized,
161
+ }
162
+ update_params = self.__clean_placeholders(update_params)
163
+
164
+ self.__auto_open()
165
+ try:
166
+ result = self.__execute_write(lambda tx: tx.run(update_query, **update_params))
167
+ records = list(result)
168
+ if not records:
169
+ raise ValueError("ATOMIC ERROR: No records updated; version may have changed")
170
+ finally:
171
+ self.__auto_close()
172
+
173
+ self.publish("update", normalized, **kwargs)
174
+ return normalized
175
+
176
+ def delete(self, **kwargs: Any) -> Dict[str, Any]:
177
+ """Remove node(s) and publish deletion events."""
178
+ node_label = kwargs.get("node") or kwargs.get("label")
179
+ if not node_label:
180
+ raise ValueError("node label must be provided for delete operations")
181
+ identifier = kwargs.get("identifier")
182
+ if not identifier:
183
+ raise ValueError("identifier must be provided for delete operations")
184
+ delete_identifier = kwargs.get("delete_identifier")
185
+ if delete_identifier is None:
186
+ raise ValueError("delete_identifier is required")
187
+
188
+ read_result = self.__get_before_delete(node_label, identifier, delete_identifier, **kwargs)
189
+ if not read_result:
190
+ return {}
191
+
192
+ delete_query = kwargs.get("delete_query")
193
+ self.__perform_delete(node_label, identifier, delete_identifier, delete_query)
194
+ self.publish("delete", read_result, **kwargs)
195
+ return read_result
196
+
197
+ def create_relationship(self, **kwargs: Any) -> Any:
198
+ """Create relationships with Cypher safeguards."""
199
+ query_text = kwargs.get("query")
200
+ if not query_text:
201
+ raise ValueError("query is required to create relationships")
202
+ if "-" not in query_text or "[" not in query_text:
203
+ raise ValueError("INTEGRITY ERROR: relationship queries must include edges")
204
+
205
+ parameters = self.__clean_placeholders(kwargs.get("placeholder") or {})
206
+ self.__auto_open()
207
+ try:
208
+ result = self.__run_write(query_text, parameters)
209
+ result_list = list(result)
210
+ self.publish("create", result_list, **kwargs) # type: ignore[arg-type]
211
+ return result_list
212
+ finally:
213
+ self.__auto_close()
214
+
215
+ def delete_relationship(self, **kwargs: Any) -> Any:
216
+ """Delete relationships while enforcing integrity constraints."""
217
+ query_text = kwargs.get("query")
218
+ if not query_text:
219
+ raise ValueError("query is required to delete relationships")
220
+ upper_query = query_text.upper()
221
+ if "DELETE" not in upper_query and "DETACH" not in upper_query:
222
+ raise ValueError("INTEGRITY ERROR: delete relationship queries must delete edges")
223
+
224
+ parameters = self.__clean_placeholders(kwargs.get("placeholder") or {})
225
+ self.__auto_open()
226
+ try:
227
+ result = self.__run_write(query_text, parameters)
228
+ result_list = list(result)
229
+ self.publish("delete", result_list, **kwargs) # type: ignore[arg-type]
230
+ return result_list
231
+ finally:
232
+ self.__auto_close()
233
+
234
+ # ---- Support utilities ----------------------------------------------------
235
+ def __map_with_schema(self, data: Dict[str, Any]) -> Dict[str, Any]:
236
+ """Normalize payloads using shared schema helpers."""
237
+ if self.schema_file and self.schema_name:
238
+ return map_to_schema(data, self.schema_file, self.schema_name)
239
+ return dict(data)
240
+
241
+ def __merge_payload(self, original: Dict[str, Any], incoming: Dict[str, Any], **kwargs: Any) -> Dict[str, Any]:
242
+ """Reuse daplug-ddb merge semantics for optimistic updates."""
243
+ return merge(original, incoming, **kwargs)
244
+
245
+ def __serialize(
246
+ self,
247
+ records: Any,
248
+ *,
249
+ node_label: Optional[str],
250
+ serialize: bool = True,
251
+ search: bool = False,
252
+ ) -> Any:
253
+ """Turn Cypher driver records into JSON-ish payloads."""
254
+ return serialize_records(records, label=node_label, serialize=serialize, search=search)
255
+
256
+ def __clean_placeholders(self, placeholder: Optional[Dict[str, Any]]) -> Dict[str, Any]:
257
+ """Convert user-supplied parameters into cypher-friendly values."""
258
+ if placeholder is None:
259
+ return {}
260
+ return convert_placeholders(placeholder)
261
+
262
+ def __resolve_bolt_config(self) -> Dict[str, Any]:
263
+ """Choose the appropriate bolt configuration (Neptune overrides)."""
264
+ return self.neptune or self.bolt
265
+
266
+ def __execute_write(self, callback) -> Any:
267
+ if not self._session:
268
+ raise ValueError("session has not been opened")
269
+ return self._session.execute_write(callback)
270
+
271
+ def __run_read(self, query: str, parameters: Dict[str, Any]) -> Any:
272
+ if not self._session:
273
+ raise ValueError("session has not been opened")
274
+ return self._session.run(query, parameters)
275
+
276
+ def __run_write(self, query: str, parameters: Dict[str, Any]) -> Any:
277
+ if not self._session:
278
+ raise ValueError("session has not been opened")
279
+ return self._session.run(query, parameters)
280
+
281
+ def __default_create_query(self, node_label: str) -> str:
282
+ return f"CREATE (n:{node_label}) SET n = $placeholder RETURN n"
283
+
284
+ def __default_update_query(self, node_label: str, identifier: str, idempotence_key: str) -> str:
285
+ return (
286
+ f"MATCH (n:{node_label}) "
287
+ f"WHERE n.{identifier} = $id AND n.{idempotence_key} = $version "
288
+ f"SET n = $placeholder RETURN n"
289
+ )
290
+
291
+ def __match(
292
+ self,
293
+ query: str,
294
+ placeholder: Optional[Dict[str, Any]],
295
+ *,
296
+ node_label: Optional[str],
297
+ serialize: bool,
298
+ search: bool,
299
+ ) -> Any:
300
+ self.__auto_open()
301
+ try:
302
+ parameters = self.__clean_placeholders(placeholder)
303
+ result = self.__run_read(query, parameters)
304
+ records = list(result)
305
+ if serialize:
306
+ return self.__serialize(records, node_label=node_label, serialize=True, search=search)
307
+ return records
308
+ finally:
309
+ self.__auto_close()
310
+
311
+ def __get_before_delete(self, node_label: str, identifier: str, delete_identifier: Any, **kwargs: Any) -> Dict[str, Any]:
312
+ read_query = kwargs.get("read_query") or f"MATCH (n:{node_label}) WHERE n.{identifier} = $id RETURN n LIMIT 1"
313
+ records = self.__match(
314
+ read_query,
315
+ {"id": delete_identifier},
316
+ node_label=node_label,
317
+ serialize=True,
318
+ search=False,
319
+ )
320
+ if isinstance(records, dict):
321
+ nodes = records.get(node_label, [])
322
+ return nodes[0] if nodes else {}
323
+ if isinstance(records, list) and records:
324
+ return records[0]
325
+ return {}
326
+
327
+ def __perform_delete(self, node_label: str, identifier: str, delete_identifier: Any, delete_query: Optional[str]) -> None:
328
+ delete_query = delete_query or (
329
+ f"MATCH (n:{node_label}) WHERE n.{identifier} = $id WITH n LIMIT 1 DETACH DELETE n"
330
+ )
331
+ parameters = self.__clean_placeholders({"id": delete_identifier})
332
+ self.__auto_open()
333
+ try:
334
+ self.__run_write(delete_query, parameters)
335
+ finally:
336
+ self.__auto_close()
337
+
338
+ def __first_node(self, record: Any) -> Optional[Any]:
339
+ if hasattr(record, "values"):
340
+ for value in record.values():
341
+ if self.__is_node(value):
342
+ return value
343
+ return None
344
+
345
+ @staticmethod
346
+ def __is_node(value: Any) -> bool:
347
+ try:
348
+ from neo4j.graph import Node # pylint: disable=import-outside-toplevel
349
+ except ImportError as exc:
350
+ raise RuntimeError("neo4j package is required for CypherAdapter") from exc
351
+ return isinstance(value, Node)
@@ -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_cypher.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_cypher.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_cypher.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,6 @@
1
+ """Cypher-specific helper stubs used by the Cypher adapter."""
2
+
3
+ from .parameters import convert_placeholders
4
+ from .serialization import serialize_records
5
+
6
+ __all__ = ["convert_placeholders", "serialize_records"]