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.
Files changed (132) hide show
  1. exonware/__init__.py +14 -0
  2. exonware/xwnode/__init__.py +127 -0
  3. exonware/xwnode/base.py +676 -0
  4. exonware/xwnode/config.py +178 -0
  5. exonware/xwnode/contracts.py +730 -0
  6. exonware/xwnode/errors.py +503 -0
  7. exonware/xwnode/facade.py +460 -0
  8. exonware/xwnode/strategies/__init__.py +158 -0
  9. exonware/xwnode/strategies/advisor.py +463 -0
  10. exonware/xwnode/strategies/edges/__init__.py +32 -0
  11. exonware/xwnode/strategies/edges/adj_list.py +227 -0
  12. exonware/xwnode/strategies/edges/adj_matrix.py +391 -0
  13. exonware/xwnode/strategies/edges/base.py +169 -0
  14. exonware/xwnode/strategies/flyweight.py +328 -0
  15. exonware/xwnode/strategies/impls/__init__.py +13 -0
  16. exonware/xwnode/strategies/impls/_base_edge.py +403 -0
  17. exonware/xwnode/strategies/impls/_base_node.py +307 -0
  18. exonware/xwnode/strategies/impls/edge_adj_list.py +353 -0
  19. exonware/xwnode/strategies/impls/edge_adj_matrix.py +445 -0
  20. exonware/xwnode/strategies/impls/edge_bidir_wrapper.py +455 -0
  21. exonware/xwnode/strategies/impls/edge_block_adj_matrix.py +539 -0
  22. exonware/xwnode/strategies/impls/edge_coo.py +533 -0
  23. exonware/xwnode/strategies/impls/edge_csc.py +447 -0
  24. exonware/xwnode/strategies/impls/edge_csr.py +492 -0
  25. exonware/xwnode/strategies/impls/edge_dynamic_adj_list.py +503 -0
  26. exonware/xwnode/strategies/impls/edge_flow_network.py +555 -0
  27. exonware/xwnode/strategies/impls/edge_hyperedge_set.py +516 -0
  28. exonware/xwnode/strategies/impls/edge_neural_graph.py +650 -0
  29. exonware/xwnode/strategies/impls/edge_octree.py +574 -0
  30. exonware/xwnode/strategies/impls/edge_property_store.py +655 -0
  31. exonware/xwnode/strategies/impls/edge_quadtree.py +519 -0
  32. exonware/xwnode/strategies/impls/edge_rtree.py +820 -0
  33. exonware/xwnode/strategies/impls/edge_temporal_edgeset.py +558 -0
  34. exonware/xwnode/strategies/impls/edge_tree_graph_basic.py +271 -0
  35. exonware/xwnode/strategies/impls/edge_weighted_graph.py +411 -0
  36. exonware/xwnode/strategies/manager.py +775 -0
  37. exonware/xwnode/strategies/metrics.py +538 -0
  38. exonware/xwnode/strategies/migration.py +432 -0
  39. exonware/xwnode/strategies/nodes/__init__.py +50 -0
  40. exonware/xwnode/strategies/nodes/_base_node.py +307 -0
  41. exonware/xwnode/strategies/nodes/adjacency_list.py +267 -0
  42. exonware/xwnode/strategies/nodes/aho_corasick.py +345 -0
  43. exonware/xwnode/strategies/nodes/array_list.py +209 -0
  44. exonware/xwnode/strategies/nodes/base.py +247 -0
  45. exonware/xwnode/strategies/nodes/deque.py +200 -0
  46. exonware/xwnode/strategies/nodes/hash_map.py +135 -0
  47. exonware/xwnode/strategies/nodes/heap.py +307 -0
  48. exonware/xwnode/strategies/nodes/linked_list.py +232 -0
  49. exonware/xwnode/strategies/nodes/node_aho_corasick.py +520 -0
  50. exonware/xwnode/strategies/nodes/node_array_list.py +175 -0
  51. exonware/xwnode/strategies/nodes/node_avl_tree.py +371 -0
  52. exonware/xwnode/strategies/nodes/node_b_plus_tree.py +542 -0
  53. exonware/xwnode/strategies/nodes/node_bitmap.py +420 -0
  54. exonware/xwnode/strategies/nodes/node_bitset_dynamic.py +513 -0
  55. exonware/xwnode/strategies/nodes/node_bloom_filter.py +347 -0
  56. exonware/xwnode/strategies/nodes/node_btree.py +357 -0
  57. exonware/xwnode/strategies/nodes/node_count_min_sketch.py +470 -0
  58. exonware/xwnode/strategies/nodes/node_cow_tree.py +473 -0
  59. exonware/xwnode/strategies/nodes/node_cuckoo_hash.py +392 -0
  60. exonware/xwnode/strategies/nodes/node_fenwick_tree.py +301 -0
  61. exonware/xwnode/strategies/nodes/node_hash_map.py +269 -0
  62. exonware/xwnode/strategies/nodes/node_heap.py +191 -0
  63. exonware/xwnode/strategies/nodes/node_hyperloglog.py +407 -0
  64. exonware/xwnode/strategies/nodes/node_linked_list.py +409 -0
  65. exonware/xwnode/strategies/nodes/node_lsm_tree.py +400 -0
  66. exonware/xwnode/strategies/nodes/node_ordered_map.py +390 -0
  67. exonware/xwnode/strategies/nodes/node_ordered_map_balanced.py +565 -0
  68. exonware/xwnode/strategies/nodes/node_patricia.py +512 -0
  69. exonware/xwnode/strategies/nodes/node_persistent_tree.py +378 -0
  70. exonware/xwnode/strategies/nodes/node_radix_trie.py +452 -0
  71. exonware/xwnode/strategies/nodes/node_red_black_tree.py +497 -0
  72. exonware/xwnode/strategies/nodes/node_roaring_bitmap.py +570 -0
  73. exonware/xwnode/strategies/nodes/node_segment_tree.py +289 -0
  74. exonware/xwnode/strategies/nodes/node_set_hash.py +354 -0
  75. exonware/xwnode/strategies/nodes/node_set_tree.py +480 -0
  76. exonware/xwnode/strategies/nodes/node_skip_list.py +316 -0
  77. exonware/xwnode/strategies/nodes/node_splay_tree.py +393 -0
  78. exonware/xwnode/strategies/nodes/node_suffix_array.py +487 -0
  79. exonware/xwnode/strategies/nodes/node_treap.py +387 -0
  80. exonware/xwnode/strategies/nodes/node_tree_graph_hybrid.py +1434 -0
  81. exonware/xwnode/strategies/nodes/node_trie.py +252 -0
  82. exonware/xwnode/strategies/nodes/node_union_find.py +187 -0
  83. exonware/xwnode/strategies/nodes/node_xdata_optimized.py +369 -0
  84. exonware/xwnode/strategies/nodes/priority_queue.py +209 -0
  85. exonware/xwnode/strategies/nodes/queue.py +161 -0
  86. exonware/xwnode/strategies/nodes/sparse_matrix.py +206 -0
  87. exonware/xwnode/strategies/nodes/stack.py +152 -0
  88. exonware/xwnode/strategies/nodes/trie.py +274 -0
  89. exonware/xwnode/strategies/nodes/union_find.py +283 -0
  90. exonware/xwnode/strategies/pattern_detector.py +603 -0
  91. exonware/xwnode/strategies/performance_monitor.py +487 -0
  92. exonware/xwnode/strategies/queries/__init__.py +24 -0
  93. exonware/xwnode/strategies/queries/base.py +236 -0
  94. exonware/xwnode/strategies/queries/cql.py +201 -0
  95. exonware/xwnode/strategies/queries/cypher.py +181 -0
  96. exonware/xwnode/strategies/queries/datalog.py +70 -0
  97. exonware/xwnode/strategies/queries/elastic_dsl.py +70 -0
  98. exonware/xwnode/strategies/queries/eql.py +70 -0
  99. exonware/xwnode/strategies/queries/flux.py +70 -0
  100. exonware/xwnode/strategies/queries/gql.py +70 -0
  101. exonware/xwnode/strategies/queries/graphql.py +240 -0
  102. exonware/xwnode/strategies/queries/gremlin.py +181 -0
  103. exonware/xwnode/strategies/queries/hiveql.py +214 -0
  104. exonware/xwnode/strategies/queries/hql.py +70 -0
  105. exonware/xwnode/strategies/queries/jmespath.py +219 -0
  106. exonware/xwnode/strategies/queries/jq.py +66 -0
  107. exonware/xwnode/strategies/queries/json_query.py +66 -0
  108. exonware/xwnode/strategies/queries/jsoniq.py +248 -0
  109. exonware/xwnode/strategies/queries/kql.py +70 -0
  110. exonware/xwnode/strategies/queries/linq.py +238 -0
  111. exonware/xwnode/strategies/queries/logql.py +70 -0
  112. exonware/xwnode/strategies/queries/mql.py +68 -0
  113. exonware/xwnode/strategies/queries/n1ql.py +210 -0
  114. exonware/xwnode/strategies/queries/partiql.py +70 -0
  115. exonware/xwnode/strategies/queries/pig.py +215 -0
  116. exonware/xwnode/strategies/queries/promql.py +70 -0
  117. exonware/xwnode/strategies/queries/sparql.py +220 -0
  118. exonware/xwnode/strategies/queries/sql.py +275 -0
  119. exonware/xwnode/strategies/queries/xml_query.py +66 -0
  120. exonware/xwnode/strategies/queries/xpath.py +223 -0
  121. exonware/xwnode/strategies/queries/xquery.py +258 -0
  122. exonware/xwnode/strategies/queries/xwnode_executor.py +332 -0
  123. exonware/xwnode/strategies/queries/xwquery_strategy.py +424 -0
  124. exonware/xwnode/strategies/registry.py +604 -0
  125. exonware/xwnode/strategies/simple.py +273 -0
  126. exonware/xwnode/strategies/utils.py +532 -0
  127. exonware/xwnode/types.py +912 -0
  128. exonware/xwnode/version.py +78 -0
  129. exonware_xwnode-0.0.1.12.dist-info/METADATA +169 -0
  130. exonware_xwnode-0.0.1.12.dist-info/RECORD +132 -0
  131. exonware_xwnode-0.0.1.12.dist-info/WHEEL +4 -0
  132. 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
+ }