sysmlpy 0.1.0__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.
Files changed (143) hide show
  1. sysmlpy/__init__.py +226 -0
  2. sysmlpy/__main__.py +75 -0
  3. sysmlpy/antlr/README.md +146 -0
  4. sysmlpy/antlr/SysMLv2Lexer.interp +695 -0
  5. sysmlpy/antlr/SysMLv2Lexer.py +1123 -0
  6. sysmlpy/antlr/SysMLv2Lexer.tokens +444 -0
  7. sysmlpy/antlr/SysMLv2Parser.interp +960 -0
  8. sysmlpy/antlr/SysMLv2Parser.py +40750 -0
  9. sysmlpy/antlr/SysMLv2Parser.tokens +444 -0
  10. sysmlpy/antlr/SysMLv2ParserListener.py +4485 -0
  11. sysmlpy/antlr/SysMLv2ParserVisitor.py +2498 -0
  12. sysmlpy/antlr/src/sysmlpy/grammar/antlr4/SysMLv2Lexer.interp +695 -0
  13. sysmlpy/antlr/src/sysmlpy/grammar/antlr4/SysMLv2Lexer.py +1123 -0
  14. sysmlpy/antlr/src/sysmlpy/grammar/antlr4/SysMLv2Lexer.tokens +444 -0
  15. sysmlpy/antlr_parser.py +185 -0
  16. sysmlpy/antlr_visitor.py +10864 -0
  17. sysmlpy/definition.py +1115 -0
  18. sysmlpy/examples/attribute_values.py +161 -0
  19. sysmlpy/examples/part_attributes.py +139 -0
  20. sysmlpy/examples/tuples_sequences.py +231 -0
  21. sysmlpy/formatting.py +47 -0
  22. sysmlpy/grammar/antlr4/README.md +30 -0
  23. sysmlpy/grammar/antlr4/SysMLv2Lexer.g4 +931 -0
  24. sysmlpy/grammar/antlr4/SysMLv2Lexer.interp +695 -0
  25. sysmlpy/grammar/antlr4/SysMLv2Lexer.py +1123 -0
  26. sysmlpy/grammar/antlr4/SysMLv2Lexer.tokens +444 -0
  27. sysmlpy/grammar/antlr4/SysMLv2Parser.g4 +2432 -0
  28. sysmlpy/grammar/antlr4/SysMLv2Parser.interp +960 -0
  29. sysmlpy/grammar/antlr4/SysMLv2Parser.py +40750 -0
  30. sysmlpy/grammar/antlr4/SysMLv2Parser.tokens +444 -0
  31. sysmlpy/grammar/antlr4/SysMLv2ParserListener.py +4485 -0
  32. sysmlpy/grammar/antlr4/SysMLv2ParserVisitor.py +2498 -0
  33. sysmlpy/grammar/antlr4/desc.xml +7 -0
  34. sysmlpy/grammar/antlr4/examples/camera.sysml +14 -0
  35. sysmlpy/grammar/antlr4/examples/toaster-system.sysml +193 -0
  36. sysmlpy/grammar/antlr4/examples/vehicle-model.sysml +118 -0
  37. sysmlpy/grammar/antlr4/pom.xml +59 -0
  38. sysmlpy/grammar/classes.py +8610 -0
  39. sysmlpy/library/LICENSE +165 -0
  40. sysmlpy/library/README.md +52 -0
  41. sysmlpy/library/__init__.py +0 -0
  42. sysmlpy/library/domain/Analysis/AnalysisTooling.sysml +34 -0
  43. sysmlpy/library/domain/Analysis/SampledFunctions.sysml +119 -0
  44. sysmlpy/library/domain/Analysis/StateSpaceRepresentation.sysml +143 -0
  45. sysmlpy/library/domain/Analysis/TradeStudies.sysml +171 -0
  46. sysmlpy/library/domain/Cause and Effect/CausationConnections.sysml +83 -0
  47. sysmlpy/library/domain/Cause and Effect/CauseAndEffect.sysml +81 -0
  48. sysmlpy/library/domain/Geometry/ShapeItems.sysml +899 -0
  49. sysmlpy/library/domain/Geometry/SpatialItems.sysml +168 -0
  50. sysmlpy/library/domain/Metadata/ImageMetadata.sysml +78 -0
  51. sysmlpy/library/domain/Metadata/ModelingMetadata.sysml +143 -0
  52. sysmlpy/library/domain/Metadata/ParametersOfInterestMetadata.sysml +39 -0
  53. sysmlpy/library/domain/Metadata/RiskMetadata.sysml +100 -0
  54. sysmlpy/library/domain/Quantities and Units/ISQ.sysml +42 -0
  55. sysmlpy/library/domain/Quantities and Units/ISQAcoustics.sysml +439 -0
  56. sysmlpy/library/domain/Quantities and Units/ISQAtomicNuclear.sysml +2726 -0
  57. sysmlpy/library/domain/Quantities and Units/ISQBase.sysml +206 -0
  58. sysmlpy/library/domain/Quantities and Units/ISQCharacteristicNumbers.sysml +1991 -0
  59. sysmlpy/library/domain/Quantities and Units/ISQChemistryMolecular.sysml +1353 -0
  60. sysmlpy/library/domain/Quantities and Units/ISQCondensedMatter.sysml +1223 -0
  61. sysmlpy/library/domain/Quantities and Units/ISQElectromagnetism.sysml +2333 -0
  62. sysmlpy/library/domain/Quantities and Units/ISQInformation.sysml +958 -0
  63. sysmlpy/library/domain/Quantities and Units/ISQLight.sysml +1537 -0
  64. sysmlpy/library/domain/Quantities and Units/ISQMechanics.sysml +1564 -0
  65. sysmlpy/library/domain/Quantities and Units/ISQSpaceTime.sysml +1169 -0
  66. sysmlpy/library/domain/Quantities and Units/ISQThermodynamics.sysml +1256 -0
  67. sysmlpy/library/domain/Quantities and Units/MeasurementRefCalculations.sysml +30 -0
  68. sysmlpy/library/domain/Quantities and Units/MeasurementReferences.sysml +526 -0
  69. sysmlpy/library/domain/Quantities and Units/Quantities.sysml +107 -0
  70. sysmlpy/library/domain/Quantities and Units/QuantityCalculations.sysml +70 -0
  71. sysmlpy/library/domain/Quantities and Units/SI.sysml +378 -0
  72. sysmlpy/library/domain/Quantities and Units/SIPrefixes.sysml +48 -0
  73. sysmlpy/library/domain/Quantities and Units/TensorCalculations.sysml +50 -0
  74. sysmlpy/library/domain/Quantities and Units/Time.sysml +274 -0
  75. sysmlpy/library/domain/Quantities and Units/USCustomaryUnits.sysml +260 -0
  76. sysmlpy/library/domain/Quantities and Units/VectorCalculations.sysml +62 -0
  77. sysmlpy/library/domain/Requirement Derivation/DerivationConnections.sysml +63 -0
  78. sysmlpy/library/domain/Requirement Derivation/RequirementDerivation.sysml +39 -0
  79. sysmlpy/library/kernel/Base.kerml +95 -0
  80. sysmlpy/library/kernel/BaseFunctions.kerml +80 -0
  81. sysmlpy/library/kernel/BooleanFunctions.kerml +22 -0
  82. sysmlpy/library/kernel/Clocks.kerml +156 -0
  83. sysmlpy/library/kernel/CollectionFunctions.kerml +68 -0
  84. sysmlpy/library/kernel/Collections.kerml +147 -0
  85. sysmlpy/library/kernel/ComplexFunctions.kerml +47 -0
  86. sysmlpy/library/kernel/ControlFunctions.kerml +117 -0
  87. sysmlpy/library/kernel/ControlPerformances.kerml +135 -0
  88. sysmlpy/library/kernel/DataFunctions.kerml +43 -0
  89. sysmlpy/library/kernel/FeatureReferencingPerformances.kerml +190 -0
  90. sysmlpy/library/kernel/IntegerFunctions.kerml +43 -0
  91. sysmlpy/library/kernel/KerML.kerml +483 -0
  92. sysmlpy/library/kernel/Links.kerml +67 -0
  93. sysmlpy/library/kernel/Metaobjects.kerml +58 -0
  94. sysmlpy/library/kernel/NaturalFunctions.kerml +27 -0
  95. sysmlpy/library/kernel/NumericalFunctions.kerml +43 -0
  96. sysmlpy/library/kernel/Objects.kerml +212 -0
  97. sysmlpy/library/kernel/Observation.kerml +161 -0
  98. sysmlpy/library/kernel/OccurrenceFunctions.kerml +154 -0
  99. sysmlpy/library/kernel/Occurrences.kerml +992 -0
  100. sysmlpy/library/kernel/Performances.kerml +293 -0
  101. sysmlpy/library/kernel/RationalFunctions.kerml +49 -0
  102. sysmlpy/library/kernel/RealFunctions.kerml +56 -0
  103. sysmlpy/library/kernel/ScalarFunctions.kerml +33 -0
  104. sysmlpy/library/kernel/ScalarValues.kerml +23 -0
  105. sysmlpy/library/kernel/SequenceFunctions.kerml +111 -0
  106. sysmlpy/library/kernel/SpatialFrames.kerml +197 -0
  107. sysmlpy/library/kernel/StatePerformances.kerml +145 -0
  108. sysmlpy/library/kernel/StringFunctions.kerml +25 -0
  109. sysmlpy/library/kernel/Transfers.kerml +281 -0
  110. sysmlpy/library/kernel/TransitionPerformances.kerml +66 -0
  111. sysmlpy/library/kernel/TrigFunctions.kerml +35 -0
  112. sysmlpy/library/kernel/Triggers.kerml +188 -0
  113. sysmlpy/library/kernel/VectorFunctions.kerml +273 -0
  114. sysmlpy/library/kernel/VectorValues.kerml +64 -0
  115. sysmlpy/library/systems/Actions.sysml +552 -0
  116. sysmlpy/library/systems/Allocations.sysml +29 -0
  117. sysmlpy/library/systems/AnalysisCases.sysml +38 -0
  118. sysmlpy/library/systems/Attributes.sysml +25 -0
  119. sysmlpy/library/systems/Calculations.sysml +37 -0
  120. sysmlpy/library/systems/Cases.sysml +71 -0
  121. sysmlpy/library/systems/Connections.sysml +60 -0
  122. sysmlpy/library/systems/Constraints.sysml +44 -0
  123. sysmlpy/library/systems/Flows.sysml +126 -0
  124. sysmlpy/library/systems/Interfaces.sysml +89 -0
  125. sysmlpy/library/systems/Items.sysml +153 -0
  126. sysmlpy/library/systems/Metadata.sysml +32 -0
  127. sysmlpy/library/systems/Parts.sysml +81 -0
  128. sysmlpy/library/systems/Ports.sysml +54 -0
  129. sysmlpy/library/systems/Requirements.sysml +194 -0
  130. sysmlpy/library/systems/StandardViewDefinitions.sysml +123 -0
  131. sysmlpy/library/systems/States.sysml +103 -0
  132. sysmlpy/library/systems/SysML.sysml +539 -0
  133. sysmlpy/library/systems/UseCases.sysml +57 -0
  134. sysmlpy/library/systems/VerificationCases.sysml +103 -0
  135. sysmlpy/library/systems/Views.sysml +164 -0
  136. sysmlpy/navigate.py +272 -0
  137. sysmlpy/store.py +547 -0
  138. sysmlpy/usage.py +2978 -0
  139. sysmlpy-0.1.0.dist-info/METADATA +230 -0
  140. sysmlpy-0.1.0.dist-info/RECORD +143 -0
  141. sysmlpy-0.1.0.dist-info/WHEEL +4 -0
  142. sysmlpy-0.1.0.dist-info/entry_points.txt +3 -0
  143. sysmlpy-0.1.0.dist-info/licenses/LICENSE +21 -0
sysmlpy/store.py ADDED
@@ -0,0 +1,547 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """
4
+ Storage abstraction layer for sysmlpy.
5
+
6
+ Provides a protocol-based storage backend that supports:
7
+ - InMemoryStore: dict/tree storage (current behavior, backward compatible)
8
+ - NetworkXStore: graph database storage (analysis, queries, scale)
9
+
10
+ Elements are identified by stable UUIDs, not names. Names remain the user-facing
11
+ identity while UUIDs provide persistence and graph connectivity.
12
+ """
13
+
14
+ from abc import ABC, abstractmethod
15
+ from typing import Any, Optional, Iterator
16
+ import uuid
17
+
18
+
19
+ # ── Relationship types ──────────────────────────────────────────────────────
20
+
21
+ REL_PARENT_CHILD = "parent_child"
22
+ REL_TYPED_BY = "typed_by"
23
+ REL_SPECIALIZES = "specializes"
24
+ REL_SUBSETS = "subsets"
25
+ REL_REDEFINES = "redefines"
26
+ REL_CONNECTS = "connects"
27
+ REL_FLOWS = "flows"
28
+ REL_TRANSITIONS = "transitions"
29
+ REL_SATISFIES = "satisfies"
30
+ REL_DERIVES = "derives"
31
+ REL_REFINES = "refines"
32
+ REL_VERIFIES = "verifies"
33
+
34
+
35
+ # ── Store Protocol ──────────────────────────────────────────────────────────
36
+
37
+ class Store(ABC):
38
+ """Abstract storage backend for sysmlpy elements.
39
+
40
+ All element data is stored as dicts keyed by UUID. Relationships are
41
+ stored as typed edges between element UUIDs.
42
+
43
+ Subclasses must implement: put, get, delete, children, parents,
44
+ relationships, and query.
45
+ """
46
+
47
+ @abstractmethod
48
+ def put(self, element_id: str, data: dict,
49
+ parent_id: Optional[str] = None,
50
+ rel_type: str = REL_PARENT_CHILD) -> None:
51
+ """Store an element.
52
+
53
+ Parameters
54
+ ----------
55
+ element_id : str
56
+ Unique identifier for the element (UUID).
57
+ data : dict
58
+ Element properties (name, sysml_type, custom attrs, etc.).
59
+ parent_id : str, optional
60
+ Parent element ID. If provided, creates a relationship edge.
61
+ rel_type : str
62
+ Type of relationship to parent (default: parent_child).
63
+ """
64
+
65
+ @abstractmethod
66
+ def get(self, element_id: str) -> Optional[dict]:
67
+ """Retrieve element data by ID.
68
+
69
+ Returns None if element does not exist.
70
+ """
71
+
72
+ @abstractmethod
73
+ def delete(self, element_id: str) -> bool:
74
+ """Remove an element and all its relationships.
75
+
76
+ Returns True if element existed and was removed.
77
+ """
78
+
79
+ @abstractmethod
80
+ def children(self, parent_id: str, rel_type: str = REL_PARENT_CHILD) -> list[str]:
81
+ """Return child element IDs for a given parent.
82
+
83
+ Parameters
84
+ ----------
85
+ parent_id : str
86
+ The parent element's UUID.
87
+ rel_type : str
88
+ Filter by relationship type (default: parent_child).
89
+
90
+ Returns
91
+ -------
92
+ list[str]
93
+ Ordered list of child element UUIDs.
94
+ """
95
+
96
+ @abstractmethod
97
+ def parents(self, child_id: str, rel_type: Optional[str] = None) -> list[str]:
98
+ """Return parent element IDs for a given child.
99
+
100
+ Parameters
101
+ ----------
102
+ child_id : str
103
+ The child element's UUID.
104
+ rel_type : str, optional
105
+ Filter by relationship type. If None, returns all parents.
106
+
107
+ Returns
108
+ -------
109
+ list[str]
110
+ List of parent element UUIDs.
111
+ """
112
+
113
+ @abstractmethod
114
+ def relationships(self, element_id: str,
115
+ rel_type: Optional[str] = None,
116
+ direction: str = "both") -> list[tuple[str, str, dict]]:
117
+ """Return relationships for an element.
118
+
119
+ Parameters
120
+ ----------
121
+ element_id : str
122
+ The element's UUID.
123
+ rel_type : str, optional
124
+ Filter by relationship type.
125
+ direction : str
126
+ "out" (edges from this element), "in" (edges to this element),
127
+ or "both" (default).
128
+
129
+ Returns
130
+ -------
131
+ list[tuple[str, str, dict]]
132
+ List of (target_id, rel_type, edge_data) tuples.
133
+ """
134
+
135
+ @abstractmethod
136
+ def query(self, **filters) -> list[str]:
137
+ """Find elements matching property filters.
138
+
139
+ Parameters
140
+ ----------
141
+ **filters : dict
142
+ Property name = value pairs to match.
143
+ Special keys:
144
+ - sysml_type: filter by element type
145
+ - name: filter by name (supports '*' wildcard)
146
+
147
+ Returns
148
+ -------
149
+ list[str]
150
+ List of matching element UUIDs.
151
+ """
152
+
153
+ @abstractmethod
154
+ def has(self, element_id: str) -> bool:
155
+ """Check if an element exists."""
156
+
157
+ @abstractmethod
158
+ def __len__(self) -> int:
159
+ """Return total number of elements in the store."""
160
+
161
+ @abstractmethod
162
+ def ids(self) -> Iterator[str]:
163
+ """Iterate over all element IDs in the store."""
164
+
165
+ @abstractmethod
166
+ def clear(self) -> None:
167
+ """Remove all elements and relationships."""
168
+
169
+ def descendants(self, root_id: str, rel_type: str = REL_PARENT_CHILD) -> list[str]:
170
+ """Return all descendant element IDs (recursive children).
171
+
172
+ Default implementation uses BFS over children(). Subclasses
173
+ with graph backends may override for better performance.
174
+ """
175
+ result = []
176
+ queue = list(self.children(root_id, rel_type))
177
+ while queue:
178
+ cid = queue.pop(0)
179
+ result.append(cid)
180
+ queue.extend(self.children(cid, rel_type))
181
+ return result
182
+
183
+ def ancestors(self, leaf_id: str, rel_type: str = REL_PARENT_CHILD) -> list[str]:
184
+ """Return all ancestor element IDs (recursive parents).
185
+
186
+ Default implementation walks up via parents(). Subclasses
187
+ with graph backends may override.
188
+ """
189
+ result = []
190
+ current = leaf_id
191
+ while True:
192
+ parents = self.parents(current, rel_type)
193
+ if not parents:
194
+ break
195
+ pid = parents[0]
196
+ result.append(pid)
197
+ current = pid
198
+ return result
199
+
200
+ def path(self, source_id: str, target_id: str,
201
+ rel_type: str = REL_PARENT_CHILD) -> Optional[list[str]]:
202
+ """Find shortest path between two elements.
203
+
204
+ Default implementation uses BFS. Graph backends override
205
+ with native shortest-path algorithms.
206
+ """
207
+ if source_id == target_id:
208
+ return [source_id]
209
+
210
+ visited = {source_id}
211
+ queue = [(source_id, [source_id])]
212
+
213
+ while queue:
214
+ current, path = queue.pop(0)
215
+ for child_id in self.children(current, rel_type):
216
+ if child_id == target_id:
217
+ return path + [child_id]
218
+ if child_id not in visited:
219
+ visited.add(child_id)
220
+ queue.append((child_id, path + [child_id]))
221
+
222
+ return None
223
+
224
+
225
+ # ── InMemory Store ──────────────────────────────────────────────────────────
226
+
227
+ class InMemoryStore(Store):
228
+ """Dict-based storage backend. Mirrors current sysmlpy behavior.
229
+
230
+ Elements are stored in a flat dict keyed by UUID. Parent-child
231
+ relationships are tracked via adjacency lists. This backend provides
232
+ O(1) lookups and is fully backward compatible with the existing API.
233
+ """
234
+
235
+ def __init__(self):
236
+ self._elements: dict[str, dict] = {}
237
+ self._children: dict[str, list[str]] = {}
238
+ self._parents: dict[str, list[str]] = {}
239
+ self._edges: dict[str, list[tuple[str, str, dict]]] = {}
240
+
241
+ def put(self, element_id: str, data: dict,
242
+ parent_id: Optional[str] = None,
243
+ rel_type: str = REL_PARENT_CHILD) -> None:
244
+ self._elements[element_id] = data
245
+
246
+ if parent_id is not None:
247
+ self._children.setdefault(parent_id, []).append(element_id)
248
+ self._parents.setdefault(element_id, []).append(parent_id)
249
+
250
+ edge_data = {"rel_type": rel_type}
251
+ self._edges.setdefault(parent_id, []).append((element_id, rel_type, edge_data))
252
+ self._edges.setdefault(element_id, []).append((parent_id, rel_type, edge_data))
253
+
254
+ def get(self, element_id: str) -> Optional[dict]:
255
+ return self._elements.get(element_id)
256
+
257
+ def delete(self, element_id: str) -> bool:
258
+ if element_id not in self._elements:
259
+ return False
260
+
261
+ del self._elements[element_id]
262
+
263
+ for pid in self._parents.pop(element_id, []):
264
+ if pid in self._children:
265
+ self._children[pid] = [c for c in self._children[pid] if c != element_id]
266
+
267
+ for cid in self._children.pop(element_id, []):
268
+ if cid in self._parents:
269
+ self._parents[cid] = [p for p in self._parents[cid] if p != element_id]
270
+
271
+ self._edges.pop(element_id, None)
272
+ for eid in list(self._edges.keys()):
273
+ self._edges[eid] = [(t, r, d) for t, r, d in self._edges[eid]
274
+ if t != element_id]
275
+
276
+ return True
277
+
278
+ def children(self, parent_id: str, rel_type: str = REL_PARENT_CHILD) -> list[str]:
279
+ return self._children.get(parent_id, []).copy()
280
+
281
+ def parents(self, child_id: str, rel_type: Optional[str] = None) -> list[str]:
282
+ return self._parents.get(child_id, []).copy()
283
+
284
+ def relationships(self, element_id: str,
285
+ rel_type: Optional[str] = None,
286
+ direction: str = "both") -> list[tuple[str, str, dict]]:
287
+ edges = self._edges.get(element_id, [])
288
+ if rel_type:
289
+ edges = [(t, r, d) for t, r, d in edges if r == rel_type]
290
+ return edges.copy()
291
+
292
+ def query(self, **filters) -> list[str]:
293
+ results = []
294
+ for eid, data in self._elements.items():
295
+ match = True
296
+ for key, value in filters.items():
297
+ if key == "name" and "*" in str(value):
298
+ import fnmatch
299
+ if not fnmatch.fnmatch(data.get("name", ""), value):
300
+ match = False
301
+ break
302
+ elif data.get(key) != value:
303
+ match = False
304
+ break
305
+ if match:
306
+ results.append(eid)
307
+ return results
308
+
309
+ def has(self, element_id: str) -> bool:
310
+ return element_id in self._elements
311
+
312
+ def __len__(self) -> int:
313
+ return len(self._elements)
314
+
315
+ def ids(self) -> Iterator[str]:
316
+ return iter(self._elements.keys())
317
+
318
+ def clear(self) -> None:
319
+ self._elements.clear()
320
+ self._children.clear()
321
+ self._parents.clear()
322
+ self._edges.clear()
323
+
324
+
325
+ # ── NetworkX Store ──────────────────────────────────────────────────────────
326
+
327
+ class NetworkXStore(Store):
328
+ """Graph database storage backend using NetworkX.
329
+
330
+ Elements are nodes with properties stored as node attributes.
331
+ Relationships are directed edges with typed labels. This backend
332
+ enables graph analysis (shortest paths, centrality, cycles) and
333
+ scales to projects that exceed simple tree structures.
334
+
335
+ Requires: pip install networkx
336
+ """
337
+
338
+ def __init__(self, directed: bool = True):
339
+ import networkx as nx
340
+ if directed:
341
+ self._graph = nx.MultiDiGraph()
342
+ else:
343
+ self._graph = nx.MultiGraph()
344
+ self._nx = nx
345
+
346
+ def put(self, element_id: str, data: dict,
347
+ parent_id: Optional[str] = None,
348
+ rel_type: str = REL_PARENT_CHILD) -> None:
349
+ self._graph.add_node(element_id, **data)
350
+
351
+ if parent_id is not None:
352
+ self._graph.add_edge(parent_id, element_id,
353
+ rel_type=rel_type, **{"_rel": rel_type})
354
+
355
+ def get(self, element_id: str) -> Optional[dict]:
356
+ if not self._graph.has_node(element_id):
357
+ return None
358
+ return dict(self._graph.nodes[element_id])
359
+
360
+ def delete(self, element_id: str) -> bool:
361
+ if not self._graph.has_node(element_id):
362
+ return False
363
+ self._graph.remove_node(element_id)
364
+ return True
365
+
366
+ def children(self, parent_id: str, rel_type: str = REL_PARENT_CHILD) -> list[str]:
367
+ if not self._graph.has_node(parent_id):
368
+ return []
369
+ result = []
370
+ for _, child, data in self._graph.out_edges(parent_id, data=True):
371
+ if data.get("rel_type") == rel_type:
372
+ result.append(child)
373
+ return result
374
+
375
+ def parents(self, child_id: str, rel_type: Optional[str] = None) -> list[str]:
376
+ if not self._graph.has_node(child_id):
377
+ return []
378
+ result = []
379
+ for parent, _, data in self._graph.in_edges(child_id, data=True):
380
+ if rel_type is None or data.get("rel_type") == rel_type:
381
+ result.append(parent)
382
+ return result
383
+
384
+ def relationships(self, element_id: str,
385
+ rel_type: Optional[str] = None,
386
+ direction: str = "both") -> list[tuple[str, str, dict]]:
387
+ if not self._graph.has_node(element_id):
388
+ return []
389
+
390
+ edges = []
391
+ if direction in ("out", "both"):
392
+ for _, target, data in self._graph.out_edges(element_id, data=True):
393
+ rt = data.get("rel_type", "")
394
+ if rel_type is None or rt == rel_type:
395
+ edges.append((target, rt, dict(data)))
396
+ if direction in ("in", "both"):
397
+ for source, _, data in self._graph.in_edges(element_id, data=True):
398
+ rt = data.get("rel_type", "")
399
+ if rel_type is None or rt == rel_type:
400
+ edges.append((source, rt, dict(data)))
401
+ return edges
402
+
403
+ def query(self, **filters) -> list[str]:
404
+ results = []
405
+ for node, data in self._graph.nodes(data=True):
406
+ match = True
407
+ for key, value in filters.items():
408
+ if key == "name" and "*" in str(value):
409
+ import fnmatch
410
+ if not fnmatch.fnmatch(data.get("name", ""), value):
411
+ match = False
412
+ break
413
+ elif data.get(key) != value:
414
+ match = False
415
+ break
416
+ if match:
417
+ results.append(node)
418
+ return results
419
+
420
+ def has(self, element_id: str) -> bool:
421
+ return self._graph.has_node(element_id)
422
+
423
+ def __len__(self) -> int:
424
+ return self._graph.number_of_nodes()
425
+
426
+ def ids(self) -> Iterator[str]:
427
+ return iter(self._graph.nodes())
428
+
429
+ def clear(self) -> None:
430
+ self._graph.clear()
431
+
432
+ # ── Graph-specific methods ──────────────────────────────────────────
433
+
434
+ def descendants(self, root_id: str, rel_type: str = REL_PARENT_CHILD) -> list[str]:
435
+ """Return all descendants via BFS on the graph."""
436
+ if not self._graph.has_node(root_id):
437
+ return []
438
+ try:
439
+ return list(self._nx.descendants(self._graph, root_id))
440
+ except self._nx.NetworkXError:
441
+ return []
442
+
443
+ def ancestors(self, leaf_id: str, rel_type: str = REL_PARENT_CHILD) -> list[str]:
444
+ """Return all ancestors via reverse BFS on the graph."""
445
+ if not self._graph.has_node(leaf_id):
446
+ return []
447
+ try:
448
+ return list(self._nx.ancestors(self._graph, leaf_id))
449
+ except self._nx.NetworkXError:
450
+ return []
451
+
452
+ def path(self, source_id: str, target_id: str,
453
+ rel_type: str = REL_PARENT_CHILD) -> Optional[list[str]]:
454
+ """Find shortest path using NetworkX native algorithm."""
455
+ if not self._graph.has_node(source_id) or not self._graph.has_node(target_id):
456
+ return None
457
+ try:
458
+ return self._nx.shortest_path(self._graph, source_id, target_id)
459
+ except self._nx.NetworkXNoPath:
460
+ return None
461
+
462
+ def connected_components(self, rel_type: Optional[str] = None) -> list[set[str]]:
463
+ """Return connected components of the graph."""
464
+ if rel_type:
465
+ subgraph = self._nx.subgraph_view(
466
+ self._graph,
467
+ edge_filter=lambda e: self._graph.edges[e].get("rel_type") == rel_type
468
+ )
469
+ return list(self._nx.weakly_connected_components(subgraph))
470
+ return list(self._nx.weakly_connected_components(self._graph))
471
+
472
+ def cycles(self, rel_type: Optional[str] = None) -> list[list[str]]:
473
+ """Find all simple cycles in the graph."""
474
+ if rel_type:
475
+ subgraph = self._nx.subgraph_view(
476
+ self._graph,
477
+ edge_filter=lambda e: self._graph.edges[e].get("rel_type") == rel_type
478
+ )
479
+ return list(self._nx.simple_cycles(subgraph))
480
+ return list(self._nx.simple_cycles(self._graph))
481
+
482
+ def centrality(self, rel_type: Optional[str] = None) -> dict[str, float]:
483
+ """Return degree centrality for all nodes."""
484
+ if rel_type:
485
+ subgraph = self._nx.subgraph_view(
486
+ self._graph,
487
+ edge_filter=lambda e: self._graph.edges[e].get("rel_type") == rel_type
488
+ )
489
+ return self._nx.degree_centrality(subgraph)
490
+ return self._nx.degree_centrality(self._graph)
491
+
492
+ def subgraph(self, element_ids: list[str]) -> "NetworkXStore":
493
+ """Return a new store containing only the specified elements."""
494
+ new_store = NetworkXStore()
495
+ new_store._graph = self._graph.subgraph(element_ids).copy()
496
+ return new_store
497
+
498
+ def export_graphml(self, path: str) -> None:
499
+ """Export the graph to GraphML format for visualization."""
500
+ self._nx.write_graphml(self._graph, path)
501
+
502
+ def stats(self) -> dict:
503
+ """Return graph statistics."""
504
+ return {
505
+ "nodes": self._graph.number_of_nodes(),
506
+ "edges": self._graph.number_of_edges(),
507
+ "density": self._nx.density(self._graph),
508
+ "is_connected": self._nx.is_weakly_connected(self._graph) if self._graph.number_of_nodes() > 0 else True,
509
+ "avg_degree": sum(dict(self._graph.degree()).values()) / max(self._graph.number_of_nodes(), 1),
510
+ }
511
+
512
+
513
+ # ── ID Generation ───────────────────────────────────────────────────────────
514
+
515
+ def new_id() -> str:
516
+ """Generate a new stable UUID for an element."""
517
+ return str(uuid.uuid4())
518
+
519
+
520
+ # ── Store Factory ───────────────────────────────────────────────────────────
521
+
522
+ def create_store(backend: str = "memory", **kwargs) -> Store:
523
+ """Create a storage backend by name.
524
+
525
+ Parameters
526
+ ----------
527
+ backend : str
528
+ "memory" for InMemoryStore, "networkx" for NetworkXStore.
529
+ **kwargs
530
+ Passed to the store constructor.
531
+
532
+ Returns
533
+ -------
534
+ Store
535
+ An initialized storage backend.
536
+ """
537
+ backends = {
538
+ "memory": InMemoryStore,
539
+ "inmemory": InMemoryStore,
540
+ "networkx": NetworkXStore,
541
+ "nx": NetworkXStore,
542
+ "graph": NetworkXStore,
543
+ }
544
+ cls = backends.get(backend.lower())
545
+ if cls is None:
546
+ raise ValueError(f"Unknown backend: {backend}. Choose from: {list(backends.keys())}")
547
+ return cls(**kwargs)