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.
- sysmlpy/__init__.py +226 -0
- sysmlpy/__main__.py +75 -0
- sysmlpy/antlr/README.md +146 -0
- sysmlpy/antlr/SysMLv2Lexer.interp +695 -0
- sysmlpy/antlr/SysMLv2Lexer.py +1123 -0
- sysmlpy/antlr/SysMLv2Lexer.tokens +444 -0
- sysmlpy/antlr/SysMLv2Parser.interp +960 -0
- sysmlpy/antlr/SysMLv2Parser.py +40750 -0
- sysmlpy/antlr/SysMLv2Parser.tokens +444 -0
- sysmlpy/antlr/SysMLv2ParserListener.py +4485 -0
- sysmlpy/antlr/SysMLv2ParserVisitor.py +2498 -0
- sysmlpy/antlr/src/sysmlpy/grammar/antlr4/SysMLv2Lexer.interp +695 -0
- sysmlpy/antlr/src/sysmlpy/grammar/antlr4/SysMLv2Lexer.py +1123 -0
- sysmlpy/antlr/src/sysmlpy/grammar/antlr4/SysMLv2Lexer.tokens +444 -0
- sysmlpy/antlr_parser.py +185 -0
- sysmlpy/antlr_visitor.py +10864 -0
- sysmlpy/definition.py +1115 -0
- sysmlpy/examples/attribute_values.py +161 -0
- sysmlpy/examples/part_attributes.py +139 -0
- sysmlpy/examples/tuples_sequences.py +231 -0
- sysmlpy/formatting.py +47 -0
- sysmlpy/grammar/antlr4/README.md +30 -0
- sysmlpy/grammar/antlr4/SysMLv2Lexer.g4 +931 -0
- sysmlpy/grammar/antlr4/SysMLv2Lexer.interp +695 -0
- sysmlpy/grammar/antlr4/SysMLv2Lexer.py +1123 -0
- sysmlpy/grammar/antlr4/SysMLv2Lexer.tokens +444 -0
- sysmlpy/grammar/antlr4/SysMLv2Parser.g4 +2432 -0
- sysmlpy/grammar/antlr4/SysMLv2Parser.interp +960 -0
- sysmlpy/grammar/antlr4/SysMLv2Parser.py +40750 -0
- sysmlpy/grammar/antlr4/SysMLv2Parser.tokens +444 -0
- sysmlpy/grammar/antlr4/SysMLv2ParserListener.py +4485 -0
- sysmlpy/grammar/antlr4/SysMLv2ParserVisitor.py +2498 -0
- sysmlpy/grammar/antlr4/desc.xml +7 -0
- sysmlpy/grammar/antlr4/examples/camera.sysml +14 -0
- sysmlpy/grammar/antlr4/examples/toaster-system.sysml +193 -0
- sysmlpy/grammar/antlr4/examples/vehicle-model.sysml +118 -0
- sysmlpy/grammar/antlr4/pom.xml +59 -0
- sysmlpy/grammar/classes.py +8610 -0
- sysmlpy/library/LICENSE +165 -0
- sysmlpy/library/README.md +52 -0
- sysmlpy/library/__init__.py +0 -0
- sysmlpy/library/domain/Analysis/AnalysisTooling.sysml +34 -0
- sysmlpy/library/domain/Analysis/SampledFunctions.sysml +119 -0
- sysmlpy/library/domain/Analysis/StateSpaceRepresentation.sysml +143 -0
- sysmlpy/library/domain/Analysis/TradeStudies.sysml +171 -0
- sysmlpy/library/domain/Cause and Effect/CausationConnections.sysml +83 -0
- sysmlpy/library/domain/Cause and Effect/CauseAndEffect.sysml +81 -0
- sysmlpy/library/domain/Geometry/ShapeItems.sysml +899 -0
- sysmlpy/library/domain/Geometry/SpatialItems.sysml +168 -0
- sysmlpy/library/domain/Metadata/ImageMetadata.sysml +78 -0
- sysmlpy/library/domain/Metadata/ModelingMetadata.sysml +143 -0
- sysmlpy/library/domain/Metadata/ParametersOfInterestMetadata.sysml +39 -0
- sysmlpy/library/domain/Metadata/RiskMetadata.sysml +100 -0
- sysmlpy/library/domain/Quantities and Units/ISQ.sysml +42 -0
- sysmlpy/library/domain/Quantities and Units/ISQAcoustics.sysml +439 -0
- sysmlpy/library/domain/Quantities and Units/ISQAtomicNuclear.sysml +2726 -0
- sysmlpy/library/domain/Quantities and Units/ISQBase.sysml +206 -0
- sysmlpy/library/domain/Quantities and Units/ISQCharacteristicNumbers.sysml +1991 -0
- sysmlpy/library/domain/Quantities and Units/ISQChemistryMolecular.sysml +1353 -0
- sysmlpy/library/domain/Quantities and Units/ISQCondensedMatter.sysml +1223 -0
- sysmlpy/library/domain/Quantities and Units/ISQElectromagnetism.sysml +2333 -0
- sysmlpy/library/domain/Quantities and Units/ISQInformation.sysml +958 -0
- sysmlpy/library/domain/Quantities and Units/ISQLight.sysml +1537 -0
- sysmlpy/library/domain/Quantities and Units/ISQMechanics.sysml +1564 -0
- sysmlpy/library/domain/Quantities and Units/ISQSpaceTime.sysml +1169 -0
- sysmlpy/library/domain/Quantities and Units/ISQThermodynamics.sysml +1256 -0
- sysmlpy/library/domain/Quantities and Units/MeasurementRefCalculations.sysml +30 -0
- sysmlpy/library/domain/Quantities and Units/MeasurementReferences.sysml +526 -0
- sysmlpy/library/domain/Quantities and Units/Quantities.sysml +107 -0
- sysmlpy/library/domain/Quantities and Units/QuantityCalculations.sysml +70 -0
- sysmlpy/library/domain/Quantities and Units/SI.sysml +378 -0
- sysmlpy/library/domain/Quantities and Units/SIPrefixes.sysml +48 -0
- sysmlpy/library/domain/Quantities and Units/TensorCalculations.sysml +50 -0
- sysmlpy/library/domain/Quantities and Units/Time.sysml +274 -0
- sysmlpy/library/domain/Quantities and Units/USCustomaryUnits.sysml +260 -0
- sysmlpy/library/domain/Quantities and Units/VectorCalculations.sysml +62 -0
- sysmlpy/library/domain/Requirement Derivation/DerivationConnections.sysml +63 -0
- sysmlpy/library/domain/Requirement Derivation/RequirementDerivation.sysml +39 -0
- sysmlpy/library/kernel/Base.kerml +95 -0
- sysmlpy/library/kernel/BaseFunctions.kerml +80 -0
- sysmlpy/library/kernel/BooleanFunctions.kerml +22 -0
- sysmlpy/library/kernel/Clocks.kerml +156 -0
- sysmlpy/library/kernel/CollectionFunctions.kerml +68 -0
- sysmlpy/library/kernel/Collections.kerml +147 -0
- sysmlpy/library/kernel/ComplexFunctions.kerml +47 -0
- sysmlpy/library/kernel/ControlFunctions.kerml +117 -0
- sysmlpy/library/kernel/ControlPerformances.kerml +135 -0
- sysmlpy/library/kernel/DataFunctions.kerml +43 -0
- sysmlpy/library/kernel/FeatureReferencingPerformances.kerml +190 -0
- sysmlpy/library/kernel/IntegerFunctions.kerml +43 -0
- sysmlpy/library/kernel/KerML.kerml +483 -0
- sysmlpy/library/kernel/Links.kerml +67 -0
- sysmlpy/library/kernel/Metaobjects.kerml +58 -0
- sysmlpy/library/kernel/NaturalFunctions.kerml +27 -0
- sysmlpy/library/kernel/NumericalFunctions.kerml +43 -0
- sysmlpy/library/kernel/Objects.kerml +212 -0
- sysmlpy/library/kernel/Observation.kerml +161 -0
- sysmlpy/library/kernel/OccurrenceFunctions.kerml +154 -0
- sysmlpy/library/kernel/Occurrences.kerml +992 -0
- sysmlpy/library/kernel/Performances.kerml +293 -0
- sysmlpy/library/kernel/RationalFunctions.kerml +49 -0
- sysmlpy/library/kernel/RealFunctions.kerml +56 -0
- sysmlpy/library/kernel/ScalarFunctions.kerml +33 -0
- sysmlpy/library/kernel/ScalarValues.kerml +23 -0
- sysmlpy/library/kernel/SequenceFunctions.kerml +111 -0
- sysmlpy/library/kernel/SpatialFrames.kerml +197 -0
- sysmlpy/library/kernel/StatePerformances.kerml +145 -0
- sysmlpy/library/kernel/StringFunctions.kerml +25 -0
- sysmlpy/library/kernel/Transfers.kerml +281 -0
- sysmlpy/library/kernel/TransitionPerformances.kerml +66 -0
- sysmlpy/library/kernel/TrigFunctions.kerml +35 -0
- sysmlpy/library/kernel/Triggers.kerml +188 -0
- sysmlpy/library/kernel/VectorFunctions.kerml +273 -0
- sysmlpy/library/kernel/VectorValues.kerml +64 -0
- sysmlpy/library/systems/Actions.sysml +552 -0
- sysmlpy/library/systems/Allocations.sysml +29 -0
- sysmlpy/library/systems/AnalysisCases.sysml +38 -0
- sysmlpy/library/systems/Attributes.sysml +25 -0
- sysmlpy/library/systems/Calculations.sysml +37 -0
- sysmlpy/library/systems/Cases.sysml +71 -0
- sysmlpy/library/systems/Connections.sysml +60 -0
- sysmlpy/library/systems/Constraints.sysml +44 -0
- sysmlpy/library/systems/Flows.sysml +126 -0
- sysmlpy/library/systems/Interfaces.sysml +89 -0
- sysmlpy/library/systems/Items.sysml +153 -0
- sysmlpy/library/systems/Metadata.sysml +32 -0
- sysmlpy/library/systems/Parts.sysml +81 -0
- sysmlpy/library/systems/Ports.sysml +54 -0
- sysmlpy/library/systems/Requirements.sysml +194 -0
- sysmlpy/library/systems/StandardViewDefinitions.sysml +123 -0
- sysmlpy/library/systems/States.sysml +103 -0
- sysmlpy/library/systems/SysML.sysml +539 -0
- sysmlpy/library/systems/UseCases.sysml +57 -0
- sysmlpy/library/systems/VerificationCases.sysml +103 -0
- sysmlpy/library/systems/Views.sysml +164 -0
- sysmlpy/navigate.py +272 -0
- sysmlpy/store.py +547 -0
- sysmlpy/usage.py +2978 -0
- sysmlpy-0.1.0.dist-info/METADATA +230 -0
- sysmlpy-0.1.0.dist-info/RECORD +143 -0
- sysmlpy-0.1.0.dist-info/WHEEL +4 -0
- sysmlpy-0.1.0.dist-info/entry_points.txt +3 -0
- 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)
|