exonware-xwnode 0.0.1.12__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.
- exonware/__init__.py +14 -0
- exonware/xwnode/__init__.py +127 -0
- exonware/xwnode/base.py +676 -0
- exonware/xwnode/config.py +178 -0
- exonware/xwnode/contracts.py +730 -0
- exonware/xwnode/errors.py +503 -0
- exonware/xwnode/facade.py +460 -0
- exonware/xwnode/strategies/__init__.py +158 -0
- exonware/xwnode/strategies/advisor.py +463 -0
- exonware/xwnode/strategies/edges/__init__.py +32 -0
- exonware/xwnode/strategies/edges/adj_list.py +227 -0
- exonware/xwnode/strategies/edges/adj_matrix.py +391 -0
- exonware/xwnode/strategies/edges/base.py +169 -0
- exonware/xwnode/strategies/flyweight.py +328 -0
- exonware/xwnode/strategies/impls/__init__.py +13 -0
- exonware/xwnode/strategies/impls/_base_edge.py +403 -0
- exonware/xwnode/strategies/impls/_base_node.py +307 -0
- exonware/xwnode/strategies/impls/edge_adj_list.py +353 -0
- exonware/xwnode/strategies/impls/edge_adj_matrix.py +445 -0
- exonware/xwnode/strategies/impls/edge_bidir_wrapper.py +455 -0
- exonware/xwnode/strategies/impls/edge_block_adj_matrix.py +539 -0
- exonware/xwnode/strategies/impls/edge_coo.py +533 -0
- exonware/xwnode/strategies/impls/edge_csc.py +447 -0
- exonware/xwnode/strategies/impls/edge_csr.py +492 -0
- exonware/xwnode/strategies/impls/edge_dynamic_adj_list.py +503 -0
- exonware/xwnode/strategies/impls/edge_flow_network.py +555 -0
- exonware/xwnode/strategies/impls/edge_hyperedge_set.py +516 -0
- exonware/xwnode/strategies/impls/edge_neural_graph.py +650 -0
- exonware/xwnode/strategies/impls/edge_octree.py +574 -0
- exonware/xwnode/strategies/impls/edge_property_store.py +655 -0
- exonware/xwnode/strategies/impls/edge_quadtree.py +519 -0
- exonware/xwnode/strategies/impls/edge_rtree.py +820 -0
- exonware/xwnode/strategies/impls/edge_temporal_edgeset.py +558 -0
- exonware/xwnode/strategies/impls/edge_tree_graph_basic.py +271 -0
- exonware/xwnode/strategies/impls/edge_weighted_graph.py +411 -0
- exonware/xwnode/strategies/manager.py +775 -0
- exonware/xwnode/strategies/metrics.py +538 -0
- exonware/xwnode/strategies/migration.py +432 -0
- exonware/xwnode/strategies/nodes/__init__.py +50 -0
- exonware/xwnode/strategies/nodes/_base_node.py +307 -0
- exonware/xwnode/strategies/nodes/adjacency_list.py +267 -0
- exonware/xwnode/strategies/nodes/aho_corasick.py +345 -0
- exonware/xwnode/strategies/nodes/array_list.py +209 -0
- exonware/xwnode/strategies/nodes/base.py +247 -0
- exonware/xwnode/strategies/nodes/deque.py +200 -0
- exonware/xwnode/strategies/nodes/hash_map.py +135 -0
- exonware/xwnode/strategies/nodes/heap.py +307 -0
- exonware/xwnode/strategies/nodes/linked_list.py +232 -0
- exonware/xwnode/strategies/nodes/node_aho_corasick.py +520 -0
- exonware/xwnode/strategies/nodes/node_array_list.py +175 -0
- exonware/xwnode/strategies/nodes/node_avl_tree.py +371 -0
- exonware/xwnode/strategies/nodes/node_b_plus_tree.py +542 -0
- exonware/xwnode/strategies/nodes/node_bitmap.py +420 -0
- exonware/xwnode/strategies/nodes/node_bitset_dynamic.py +513 -0
- exonware/xwnode/strategies/nodes/node_bloom_filter.py +347 -0
- exonware/xwnode/strategies/nodes/node_btree.py +357 -0
- exonware/xwnode/strategies/nodes/node_count_min_sketch.py +470 -0
- exonware/xwnode/strategies/nodes/node_cow_tree.py +473 -0
- exonware/xwnode/strategies/nodes/node_cuckoo_hash.py +392 -0
- exonware/xwnode/strategies/nodes/node_fenwick_tree.py +301 -0
- exonware/xwnode/strategies/nodes/node_hash_map.py +269 -0
- exonware/xwnode/strategies/nodes/node_heap.py +191 -0
- exonware/xwnode/strategies/nodes/node_hyperloglog.py +407 -0
- exonware/xwnode/strategies/nodes/node_linked_list.py +409 -0
- exonware/xwnode/strategies/nodes/node_lsm_tree.py +400 -0
- exonware/xwnode/strategies/nodes/node_ordered_map.py +390 -0
- exonware/xwnode/strategies/nodes/node_ordered_map_balanced.py +565 -0
- exonware/xwnode/strategies/nodes/node_patricia.py +512 -0
- exonware/xwnode/strategies/nodes/node_persistent_tree.py +378 -0
- exonware/xwnode/strategies/nodes/node_radix_trie.py +452 -0
- exonware/xwnode/strategies/nodes/node_red_black_tree.py +497 -0
- exonware/xwnode/strategies/nodes/node_roaring_bitmap.py +570 -0
- exonware/xwnode/strategies/nodes/node_segment_tree.py +289 -0
- exonware/xwnode/strategies/nodes/node_set_hash.py +354 -0
- exonware/xwnode/strategies/nodes/node_set_tree.py +480 -0
- exonware/xwnode/strategies/nodes/node_skip_list.py +316 -0
- exonware/xwnode/strategies/nodes/node_splay_tree.py +393 -0
- exonware/xwnode/strategies/nodes/node_suffix_array.py +487 -0
- exonware/xwnode/strategies/nodes/node_treap.py +387 -0
- exonware/xwnode/strategies/nodes/node_tree_graph_hybrid.py +1434 -0
- exonware/xwnode/strategies/nodes/node_trie.py +252 -0
- exonware/xwnode/strategies/nodes/node_union_find.py +187 -0
- exonware/xwnode/strategies/nodes/node_xdata_optimized.py +369 -0
- exonware/xwnode/strategies/nodes/priority_queue.py +209 -0
- exonware/xwnode/strategies/nodes/queue.py +161 -0
- exonware/xwnode/strategies/nodes/sparse_matrix.py +206 -0
- exonware/xwnode/strategies/nodes/stack.py +152 -0
- exonware/xwnode/strategies/nodes/trie.py +274 -0
- exonware/xwnode/strategies/nodes/union_find.py +283 -0
- exonware/xwnode/strategies/pattern_detector.py +603 -0
- exonware/xwnode/strategies/performance_monitor.py +487 -0
- exonware/xwnode/strategies/queries/__init__.py +24 -0
- exonware/xwnode/strategies/queries/base.py +236 -0
- exonware/xwnode/strategies/queries/cql.py +201 -0
- exonware/xwnode/strategies/queries/cypher.py +181 -0
- exonware/xwnode/strategies/queries/datalog.py +70 -0
- exonware/xwnode/strategies/queries/elastic_dsl.py +70 -0
- exonware/xwnode/strategies/queries/eql.py +70 -0
- exonware/xwnode/strategies/queries/flux.py +70 -0
- exonware/xwnode/strategies/queries/gql.py +70 -0
- exonware/xwnode/strategies/queries/graphql.py +240 -0
- exonware/xwnode/strategies/queries/gremlin.py +181 -0
- exonware/xwnode/strategies/queries/hiveql.py +214 -0
- exonware/xwnode/strategies/queries/hql.py +70 -0
- exonware/xwnode/strategies/queries/jmespath.py +219 -0
- exonware/xwnode/strategies/queries/jq.py +66 -0
- exonware/xwnode/strategies/queries/json_query.py +66 -0
- exonware/xwnode/strategies/queries/jsoniq.py +248 -0
- exonware/xwnode/strategies/queries/kql.py +70 -0
- exonware/xwnode/strategies/queries/linq.py +238 -0
- exonware/xwnode/strategies/queries/logql.py +70 -0
- exonware/xwnode/strategies/queries/mql.py +68 -0
- exonware/xwnode/strategies/queries/n1ql.py +210 -0
- exonware/xwnode/strategies/queries/partiql.py +70 -0
- exonware/xwnode/strategies/queries/pig.py +215 -0
- exonware/xwnode/strategies/queries/promql.py +70 -0
- exonware/xwnode/strategies/queries/sparql.py +220 -0
- exonware/xwnode/strategies/queries/sql.py +275 -0
- exonware/xwnode/strategies/queries/xml_query.py +66 -0
- exonware/xwnode/strategies/queries/xpath.py +223 -0
- exonware/xwnode/strategies/queries/xquery.py +258 -0
- exonware/xwnode/strategies/queries/xwnode_executor.py +332 -0
- exonware/xwnode/strategies/queries/xwquery_strategy.py +424 -0
- exonware/xwnode/strategies/registry.py +604 -0
- exonware/xwnode/strategies/simple.py +273 -0
- exonware/xwnode/strategies/utils.py +532 -0
- exonware/xwnode/types.py +912 -0
- exonware/xwnode/version.py +78 -0
- exonware_xwnode-0.0.1.12.dist-info/METADATA +169 -0
- exonware_xwnode-0.0.1.12.dist-info/RECORD +132 -0
- exonware_xwnode-0.0.1.12.dist-info/WHEEL +4 -0
- exonware_xwnode-0.0.1.12.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,503 @@
|
|
1
|
+
"""
|
2
|
+
Dynamic Adjacency List Edge Strategy Implementation
|
3
|
+
|
4
|
+
This module implements the DYNAMIC_ADJ_LIST strategy for efficiently handling
|
5
|
+
graphs with frequent structural changes and dynamic edge properties.
|
6
|
+
"""
|
7
|
+
|
8
|
+
from typing import Any, Iterator, Dict, List, Set, Optional, Tuple, DefaultDict
|
9
|
+
from collections import defaultdict, deque
|
10
|
+
import time
|
11
|
+
import threading
|
12
|
+
from ._base_edge import aEdgeStrategy
|
13
|
+
from ...types import EdgeMode, EdgeTrait
|
14
|
+
|
15
|
+
|
16
|
+
class VersionedEdge:
|
17
|
+
"""Represents an edge with version history for dynamic updates."""
|
18
|
+
|
19
|
+
def __init__(self, edge_id: str, source: str, target: str, **properties):
|
20
|
+
self.edge_id = edge_id
|
21
|
+
self.source = source
|
22
|
+
self.target = target
|
23
|
+
self.created_at = time.time()
|
24
|
+
self.updated_at = self.created_at
|
25
|
+
self.version = 1
|
26
|
+
self.properties = properties.copy()
|
27
|
+
self.is_active = True
|
28
|
+
self.history: List[Dict[str, Any]] = []
|
29
|
+
|
30
|
+
def update_properties(self, **new_properties) -> None:
|
31
|
+
"""Update edge properties with versioning."""
|
32
|
+
# Save current state to history
|
33
|
+
self.history.append({
|
34
|
+
'version': self.version,
|
35
|
+
'timestamp': self.updated_at,
|
36
|
+
'properties': self.properties.copy()
|
37
|
+
})
|
38
|
+
|
39
|
+
# Update to new state
|
40
|
+
self.properties.update(new_properties)
|
41
|
+
self.updated_at = time.time()
|
42
|
+
self.version += 1
|
43
|
+
|
44
|
+
def get_history(self) -> List[Dict[str, Any]]:
|
45
|
+
"""Get the version history of this edge."""
|
46
|
+
return self.history.copy()
|
47
|
+
|
48
|
+
def to_dict(self) -> Dict[str, Any]:
|
49
|
+
"""Convert to dictionary representation."""
|
50
|
+
return {
|
51
|
+
'id': self.edge_id,
|
52
|
+
'source': self.source,
|
53
|
+
'target': self.target,
|
54
|
+
'properties': self.properties,
|
55
|
+
'created_at': self.created_at,
|
56
|
+
'updated_at': self.updated_at,
|
57
|
+
'version': self.version,
|
58
|
+
'is_active': self.is_active
|
59
|
+
}
|
60
|
+
|
61
|
+
|
62
|
+
class xDynamicAdjListStrategy(aEdgeStrategy):
|
63
|
+
"""
|
64
|
+
Dynamic Adjacency List edge strategy for frequently changing graphs.
|
65
|
+
|
66
|
+
Optimized for graphs with frequent edge additions, removals, and property updates.
|
67
|
+
Provides efficient batch operations, change tracking, and version history.
|
68
|
+
"""
|
69
|
+
|
70
|
+
def __init__(self, traits: EdgeTrait = EdgeTrait.NONE, **options):
|
71
|
+
"""Initialize the Dynamic Adjacency List strategy."""
|
72
|
+
super().__init__(EdgeMode.DYNAMIC_ADJ_LIST, traits, **options)
|
73
|
+
|
74
|
+
self.is_directed = options.get('directed', True)
|
75
|
+
self.track_history = options.get('track_history', True)
|
76
|
+
self.max_history_per_edge = options.get('max_history_per_edge', 10)
|
77
|
+
self.enable_batching = options.get('enable_batching', True)
|
78
|
+
|
79
|
+
# Core storage with dynamic updates in mind
|
80
|
+
self._outgoing: DefaultDict[str, Dict[str, VersionedEdge]] = defaultdict(dict)
|
81
|
+
self._incoming: DefaultDict[str, Dict[str, VersionedEdge]] = defaultdict(dict) if self.is_directed else None
|
82
|
+
|
83
|
+
# Vertex management
|
84
|
+
self._vertices: Set[str] = set()
|
85
|
+
|
86
|
+
# Change tracking
|
87
|
+
self._edge_count = 0
|
88
|
+
self._edge_id_counter = 0
|
89
|
+
self._change_log: deque = deque(maxlen=1000) # Recent changes
|
90
|
+
self._batch_operations: List[Dict[str, Any]] = []
|
91
|
+
|
92
|
+
# Performance optimizations
|
93
|
+
self._lock = threading.RLock() if options.get('thread_safe', False) else None
|
94
|
+
self._dirty_vertices: Set[str] = set() # Vertices with pending updates
|
95
|
+
|
96
|
+
def get_supported_traits(self) -> EdgeTrait:
|
97
|
+
"""Get the traits supported by the dynamic adjacency list strategy."""
|
98
|
+
return (EdgeTrait.SPARSE | EdgeTrait.DIRECTED | EdgeTrait.WEIGHTED |
|
99
|
+
EdgeTrait.MULTI | EdgeTrait.TEMPORAL)
|
100
|
+
|
101
|
+
def _with_lock(self, func):
|
102
|
+
"""Execute function with lock if thread safety is enabled."""
|
103
|
+
if self._lock:
|
104
|
+
with self._lock:
|
105
|
+
return func()
|
106
|
+
else:
|
107
|
+
return func()
|
108
|
+
|
109
|
+
def _log_change(self, operation: str, **details) -> None:
|
110
|
+
"""Log a change operation."""
|
111
|
+
change_record = {
|
112
|
+
'timestamp': time.time(),
|
113
|
+
'operation': operation,
|
114
|
+
'details': details
|
115
|
+
}
|
116
|
+
self._change_log.append(change_record)
|
117
|
+
|
118
|
+
# ============================================================================
|
119
|
+
# CORE EDGE OPERATIONS
|
120
|
+
# ============================================================================
|
121
|
+
|
122
|
+
def add_edge(self, source: str, target: str, **properties) -> str:
|
123
|
+
"""Add an edge with dynamic property support."""
|
124
|
+
def _add():
|
125
|
+
# Generate edge ID
|
126
|
+
edge_id = f"edge_{self._edge_id_counter}"
|
127
|
+
self._edge_id_counter += 1
|
128
|
+
|
129
|
+
# Create versioned edge
|
130
|
+
edge = VersionedEdge(edge_id, source, target, **properties)
|
131
|
+
|
132
|
+
# Add vertices
|
133
|
+
self._vertices.add(source)
|
134
|
+
self._vertices.add(target)
|
135
|
+
|
136
|
+
# Store edge
|
137
|
+
self._outgoing[source][target] = edge
|
138
|
+
|
139
|
+
if self.is_directed and self._incoming is not None:
|
140
|
+
self._incoming[target][source] = edge
|
141
|
+
elif not self.is_directed and source != target:
|
142
|
+
self._outgoing[target][source] = edge
|
143
|
+
|
144
|
+
self._edge_count += 1
|
145
|
+
self._dirty_vertices.add(source)
|
146
|
+
self._dirty_vertices.add(target)
|
147
|
+
|
148
|
+
# Log change
|
149
|
+
self._log_change('add_edge', edge_id=edge_id, source=source,
|
150
|
+
target=target, properties=properties)
|
151
|
+
|
152
|
+
return edge_id
|
153
|
+
|
154
|
+
return self._with_lock(_add)
|
155
|
+
|
156
|
+
def remove_edge(self, source: str, target: str, edge_id: Optional[str] = None) -> bool:
|
157
|
+
"""Remove edge with change tracking."""
|
158
|
+
def _remove():
|
159
|
+
if source not in self._outgoing or target not in self._outgoing[source]:
|
160
|
+
return False
|
161
|
+
|
162
|
+
edge = self._outgoing[source][target]
|
163
|
+
|
164
|
+
# Check edge ID if specified
|
165
|
+
if edge_id and edge.edge_id != edge_id:
|
166
|
+
return False
|
167
|
+
|
168
|
+
# Mark as inactive instead of deleting (for history)
|
169
|
+
if self.track_history:
|
170
|
+
edge.is_active = False
|
171
|
+
edge.updated_at = time.time()
|
172
|
+
else:
|
173
|
+
del self._outgoing[source][target]
|
174
|
+
|
175
|
+
# Remove from incoming list
|
176
|
+
if self.is_directed and self._incoming is not None:
|
177
|
+
if target in self._incoming and source in self._incoming[target]:
|
178
|
+
if self.track_history:
|
179
|
+
self._incoming[target][source].is_active = False
|
180
|
+
else:
|
181
|
+
del self._incoming[target][source]
|
182
|
+
elif not self.is_directed and source != target:
|
183
|
+
if target in self._outgoing and source in self._outgoing[target]:
|
184
|
+
if self.track_history:
|
185
|
+
self._outgoing[target][source].is_active = False
|
186
|
+
else:
|
187
|
+
del self._outgoing[target][source]
|
188
|
+
|
189
|
+
self._edge_count -= 1
|
190
|
+
self._dirty_vertices.add(source)
|
191
|
+
self._dirty_vertices.add(target)
|
192
|
+
|
193
|
+
# Log change
|
194
|
+
self._log_change('remove_edge', edge_id=edge.edge_id,
|
195
|
+
source=source, target=target)
|
196
|
+
|
197
|
+
return True
|
198
|
+
|
199
|
+
return self._with_lock(_remove)
|
200
|
+
|
201
|
+
def has_edge(self, source: str, target: str) -> bool:
|
202
|
+
"""Check if active edge exists."""
|
203
|
+
def _has():
|
204
|
+
if source not in self._outgoing or target not in self._outgoing[source]:
|
205
|
+
return False
|
206
|
+
|
207
|
+
edge = self._outgoing[source][target]
|
208
|
+
return edge.is_active
|
209
|
+
|
210
|
+
return self._with_lock(_has)
|
211
|
+
|
212
|
+
def get_edge_data(self, source: str, target: str) -> Optional[Dict[str, Any]]:
|
213
|
+
"""Get edge data with version information."""
|
214
|
+
def _get():
|
215
|
+
if source not in self._outgoing or target not in self._outgoing[source]:
|
216
|
+
return None
|
217
|
+
|
218
|
+
edge = self._outgoing[source][target]
|
219
|
+
if not edge.is_active:
|
220
|
+
return None
|
221
|
+
|
222
|
+
return edge.to_dict()
|
223
|
+
|
224
|
+
return self._with_lock(_get)
|
225
|
+
|
226
|
+
def update_edge_properties(self, source: str, target: str, **properties) -> bool:
|
227
|
+
"""Update edge properties with versioning."""
|
228
|
+
def _update():
|
229
|
+
if source not in self._outgoing or target not in self._outgoing[source]:
|
230
|
+
return False
|
231
|
+
|
232
|
+
edge = self._outgoing[source][target]
|
233
|
+
if not edge.is_active:
|
234
|
+
return False
|
235
|
+
|
236
|
+
edge.update_properties(**properties)
|
237
|
+
|
238
|
+
# Trim history if needed
|
239
|
+
if len(edge.history) > self.max_history_per_edge:
|
240
|
+
edge.history = edge.history[-self.max_history_per_edge:]
|
241
|
+
|
242
|
+
self._dirty_vertices.add(source)
|
243
|
+
self._dirty_vertices.add(target)
|
244
|
+
|
245
|
+
# Log change
|
246
|
+
self._log_change('update_edge', edge_id=edge.edge_id,
|
247
|
+
source=source, target=target, properties=properties)
|
248
|
+
|
249
|
+
return True
|
250
|
+
|
251
|
+
return self._with_lock(_update)
|
252
|
+
|
253
|
+
def neighbors(self, vertex: str, direction: str = 'out') -> Iterator[str]:
|
254
|
+
"""Get neighbors with active edge filtering."""
|
255
|
+
def _neighbors():
|
256
|
+
if direction == 'out':
|
257
|
+
if vertex in self._outgoing:
|
258
|
+
for target, edge in self._outgoing[vertex].items():
|
259
|
+
if edge.is_active:
|
260
|
+
yield target
|
261
|
+
elif direction == 'in':
|
262
|
+
if self.is_directed and self._incoming is not None:
|
263
|
+
if vertex in self._incoming:
|
264
|
+
for source, edge in self._incoming[vertex].items():
|
265
|
+
if edge.is_active:
|
266
|
+
yield source
|
267
|
+
elif not self.is_directed:
|
268
|
+
if vertex in self._outgoing:
|
269
|
+
for neighbor, edge in self._outgoing[vertex].items():
|
270
|
+
if edge.is_active:
|
271
|
+
yield neighbor
|
272
|
+
elif direction == 'both':
|
273
|
+
seen = set()
|
274
|
+
for neighbor in self.neighbors(vertex, 'out'):
|
275
|
+
if neighbor not in seen:
|
276
|
+
seen.add(neighbor)
|
277
|
+
yield neighbor
|
278
|
+
for neighbor in self.neighbors(vertex, 'in'):
|
279
|
+
if neighbor not in seen:
|
280
|
+
seen.add(neighbor)
|
281
|
+
yield neighbor
|
282
|
+
|
283
|
+
return self._with_lock(_neighbors)
|
284
|
+
|
285
|
+
def degree(self, vertex: str, direction: str = 'out') -> int:
|
286
|
+
"""Get degree counting only active edges."""
|
287
|
+
return sum(1 for _ in self.neighbors(vertex, direction))
|
288
|
+
|
289
|
+
def edges(self, data: bool = False, include_inactive: bool = False) -> Iterator[tuple]:
|
290
|
+
"""Get all edges with optional inactive edge inclusion."""
|
291
|
+
def _edges():
|
292
|
+
seen_edges = set()
|
293
|
+
|
294
|
+
for source, adj_dict in self._outgoing.items():
|
295
|
+
for target, edge in adj_dict.items():
|
296
|
+
if edge.is_active or include_inactive:
|
297
|
+
edge_key = (source, target, edge.edge_id)
|
298
|
+
|
299
|
+
if edge_key not in seen_edges:
|
300
|
+
seen_edges.add(edge_key)
|
301
|
+
|
302
|
+
if data:
|
303
|
+
yield (source, target, edge.to_dict())
|
304
|
+
else:
|
305
|
+
yield (source, target)
|
306
|
+
|
307
|
+
return self._with_lock(_edges)
|
308
|
+
|
309
|
+
def vertices(self) -> Iterator[str]:
|
310
|
+
"""Get all vertices."""
|
311
|
+
return iter(self._vertices)
|
312
|
+
|
313
|
+
def __len__(self) -> int:
|
314
|
+
"""Get the number of active edges."""
|
315
|
+
return self._edge_count
|
316
|
+
|
317
|
+
def vertex_count(self) -> int:
|
318
|
+
"""Get the number of vertices."""
|
319
|
+
return len(self._vertices)
|
320
|
+
|
321
|
+
def clear(self) -> None:
|
322
|
+
"""Clear all edges and vertices."""
|
323
|
+
def _clear():
|
324
|
+
self._outgoing.clear()
|
325
|
+
if self._incoming is not None:
|
326
|
+
self._incoming.clear()
|
327
|
+
self._vertices.clear()
|
328
|
+
self._edge_count = 0
|
329
|
+
self._edge_id_counter = 0
|
330
|
+
self._change_log.clear()
|
331
|
+
self._batch_operations.clear()
|
332
|
+
self._dirty_vertices.clear()
|
333
|
+
|
334
|
+
self._log_change('clear_all')
|
335
|
+
|
336
|
+
self._with_lock(_clear)
|
337
|
+
|
338
|
+
def add_vertex(self, vertex: str) -> None:
|
339
|
+
"""Add a vertex."""
|
340
|
+
def _add():
|
341
|
+
self._vertices.add(vertex)
|
342
|
+
self._log_change('add_vertex', vertex=vertex)
|
343
|
+
|
344
|
+
self._with_lock(_add)
|
345
|
+
|
346
|
+
def remove_vertex(self, vertex: str) -> bool:
|
347
|
+
"""Remove a vertex and all its edges."""
|
348
|
+
def _remove():
|
349
|
+
if vertex not in self._vertices:
|
350
|
+
return False
|
351
|
+
|
352
|
+
# Count edges to remove
|
353
|
+
edges_removed = 0
|
354
|
+
|
355
|
+
# Remove outgoing edges
|
356
|
+
if vertex in self._outgoing:
|
357
|
+
edges_removed += len([e for e in self._outgoing[vertex].values() if e.is_active])
|
358
|
+
for edge in self._outgoing[vertex].values():
|
359
|
+
if edge.is_active:
|
360
|
+
edge.is_active = False
|
361
|
+
edge.updated_at = time.time()
|
362
|
+
|
363
|
+
# Remove incoming edges
|
364
|
+
for source, adj_dict in self._outgoing.items():
|
365
|
+
if vertex in adj_dict and adj_dict[vertex].is_active:
|
366
|
+
adj_dict[vertex].is_active = False
|
367
|
+
adj_dict[vertex].updated_at = time.time()
|
368
|
+
edges_removed += 1
|
369
|
+
|
370
|
+
self._edge_count -= edges_removed
|
371
|
+
self._vertices.remove(vertex)
|
372
|
+
|
373
|
+
self._log_change('remove_vertex', vertex=vertex, edges_removed=edges_removed)
|
374
|
+
return True
|
375
|
+
|
376
|
+
return self._with_lock(_remove)
|
377
|
+
|
378
|
+
# ============================================================================
|
379
|
+
# DYNAMIC-SPECIFIC OPERATIONS
|
380
|
+
# ============================================================================
|
381
|
+
|
382
|
+
def start_batch(self) -> None:
|
383
|
+
"""Start batch operation mode."""
|
384
|
+
if self.enable_batching:
|
385
|
+
self._batch_operations.clear()
|
386
|
+
|
387
|
+
def end_batch(self) -> int:
|
388
|
+
"""End batch operation mode and return number of operations."""
|
389
|
+
if self.enable_batching:
|
390
|
+
operations_count = len(self._batch_operations)
|
391
|
+
self._batch_operations.clear()
|
392
|
+
return operations_count
|
393
|
+
return 0
|
394
|
+
|
395
|
+
def get_change_log(self, limit: int = 100) -> List[Dict[str, Any]]:
|
396
|
+
"""Get recent change history."""
|
397
|
+
return list(self._change_log)[-limit:] if self._change_log else []
|
398
|
+
|
399
|
+
def get_edge_history(self, source: str, target: str) -> List[Dict[str, Any]]:
|
400
|
+
"""Get version history for a specific edge."""
|
401
|
+
if source not in self._outgoing or target not in self._outgoing[source]:
|
402
|
+
return []
|
403
|
+
|
404
|
+
edge = self._outgoing[source][target]
|
405
|
+
return edge.get_history()
|
406
|
+
|
407
|
+
def get_dirty_vertices(self) -> Set[str]:
|
408
|
+
"""Get vertices that have pending updates."""
|
409
|
+
return self._dirty_vertices.copy()
|
410
|
+
|
411
|
+
def mark_clean(self, vertex: str) -> None:
|
412
|
+
"""Mark a vertex as clean (no pending updates)."""
|
413
|
+
self._dirty_vertices.discard(vertex)
|
414
|
+
|
415
|
+
def mark_all_clean(self) -> None:
|
416
|
+
"""Mark all vertices as clean."""
|
417
|
+
self._dirty_vertices.clear()
|
418
|
+
|
419
|
+
def compact_history(self) -> int:
|
420
|
+
"""Compact edge histories and remove inactive edges."""
|
421
|
+
def _compact():
|
422
|
+
compacted_count = 0
|
423
|
+
|
424
|
+
for source, adj_dict in self._outgoing.items():
|
425
|
+
to_remove = []
|
426
|
+
for target, edge in adj_dict.items():
|
427
|
+
if not edge.is_active and not self.track_history:
|
428
|
+
to_remove.append(target)
|
429
|
+
elif edge.is_active and len(edge.history) > self.max_history_per_edge:
|
430
|
+
# Keep only recent history
|
431
|
+
edge.history = edge.history[-self.max_history_per_edge:]
|
432
|
+
compacted_count += 1
|
433
|
+
|
434
|
+
for target in to_remove:
|
435
|
+
del adj_dict[target]
|
436
|
+
compacted_count += 1
|
437
|
+
|
438
|
+
self._log_change('compact_history', compacted_count=compacted_count)
|
439
|
+
return compacted_count
|
440
|
+
|
441
|
+
return self._with_lock(_compact)
|
442
|
+
|
443
|
+
def get_temporal_edges(self, start_time: float, end_time: float) -> List[Tuple[str, str, Dict[str, Any]]]:
|
444
|
+
"""Get edges that were active during the specified time period."""
|
445
|
+
result = []
|
446
|
+
|
447
|
+
for source, adj_dict in self._outgoing.items():
|
448
|
+
for target, edge in adj_dict.items():
|
449
|
+
if (edge.created_at <= end_time and
|
450
|
+
(edge.is_active or edge.updated_at >= start_time)):
|
451
|
+
result.append((source, target, edge.to_dict()))
|
452
|
+
|
453
|
+
return result
|
454
|
+
|
455
|
+
# ============================================================================
|
456
|
+
# PERFORMANCE CHARACTERISTICS
|
457
|
+
# ============================================================================
|
458
|
+
|
459
|
+
@property
|
460
|
+
def backend_info(self) -> Dict[str, Any]:
|
461
|
+
"""Get backend implementation info."""
|
462
|
+
return {
|
463
|
+
'strategy': 'DYNAMIC_ADJ_LIST',
|
464
|
+
'backend': 'Versioned adjacency lists with change tracking',
|
465
|
+
'directed': self.is_directed,
|
466
|
+
'track_history': self.track_history,
|
467
|
+
'enable_batching': self.enable_batching,
|
468
|
+
'thread_safe': self._lock is not None,
|
469
|
+
'complexity': {
|
470
|
+
'add_edge': 'O(1)',
|
471
|
+
'remove_edge': 'O(1)',
|
472
|
+
'update_edge': 'O(1)',
|
473
|
+
'has_edge': 'O(1)',
|
474
|
+
'neighbors': 'O(degree)',
|
475
|
+
'space': 'O(V + E + H)' # H = history
|
476
|
+
}
|
477
|
+
}
|
478
|
+
|
479
|
+
@property
|
480
|
+
def metrics(self) -> Dict[str, Any]:
|
481
|
+
"""Get performance metrics."""
|
482
|
+
total_history_entries = sum(
|
483
|
+
len(edge.history)
|
484
|
+
for adj_dict in self._outgoing.values()
|
485
|
+
for edge in adj_dict.values()
|
486
|
+
)
|
487
|
+
|
488
|
+
active_edges = sum(
|
489
|
+
1 for adj_dict in self._outgoing.values()
|
490
|
+
for edge in adj_dict.values()
|
491
|
+
if edge.is_active
|
492
|
+
)
|
493
|
+
|
494
|
+
return {
|
495
|
+
'vertices': len(self._vertices),
|
496
|
+
'active_edges': active_edges,
|
497
|
+
'total_edges': sum(len(adj_dict) for adj_dict in self._outgoing.values()),
|
498
|
+
'dirty_vertices': len(self._dirty_vertices),
|
499
|
+
'change_log_size': len(self._change_log),
|
500
|
+
'total_history_entries': total_history_entries,
|
501
|
+
'avg_history_per_edge': total_history_entries / max(1, active_edges),
|
502
|
+
'memory_usage': f"{active_edges * 120 + total_history_entries * 80} bytes (estimated)"
|
503
|
+
}
|