exonware-xwnode 0.0.1.21__py3-none-any.whl → 0.0.1.23__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 (250) hide show
  1. exonware/__init__.py +8 -1
  2. exonware/xwnode/__init__.py +18 -5
  3. exonware/xwnode/add_strategy_types.py +165 -0
  4. exonware/xwnode/base.py +7 -5
  5. exonware/xwnode/common/__init__.py +1 -1
  6. exonware/xwnode/common/graph/__init__.py +30 -0
  7. exonware/xwnode/common/graph/caching.py +131 -0
  8. exonware/xwnode/common/graph/contracts.py +100 -0
  9. exonware/xwnode/common/graph/errors.py +44 -0
  10. exonware/xwnode/common/graph/indexing.py +260 -0
  11. exonware/xwnode/common/graph/manager.py +568 -0
  12. exonware/xwnode/common/management/__init__.py +3 -5
  13. exonware/xwnode/common/management/manager.py +9 -9
  14. exonware/xwnode/common/management/migration.py +6 -6
  15. exonware/xwnode/common/monitoring/__init__.py +3 -5
  16. exonware/xwnode/common/monitoring/metrics.py +7 -3
  17. exonware/xwnode/common/monitoring/pattern_detector.py +2 -2
  18. exonware/xwnode/common/monitoring/performance_monitor.py +6 -2
  19. exonware/xwnode/common/patterns/__init__.py +3 -5
  20. exonware/xwnode/common/patterns/advisor.py +1 -1
  21. exonware/xwnode/common/patterns/flyweight.py +6 -2
  22. exonware/xwnode/common/patterns/registry.py +203 -184
  23. exonware/xwnode/common/utils/__init__.py +25 -11
  24. exonware/xwnode/common/utils/simple.py +1 -1
  25. exonware/xwnode/config.py +3 -8
  26. exonware/xwnode/contracts.py +4 -105
  27. exonware/xwnode/defs.py +413 -159
  28. exonware/xwnode/edges/strategies/__init__.py +86 -4
  29. exonware/xwnode/edges/strategies/_base_edge.py +2 -2
  30. exonware/xwnode/edges/strategies/adj_list.py +287 -121
  31. exonware/xwnode/edges/strategies/adj_matrix.py +316 -222
  32. exonware/xwnode/edges/strategies/base.py +1 -1
  33. exonware/xwnode/edges/strategies/{edge_bidir_wrapper.py → bidir_wrapper.py} +45 -4
  34. exonware/xwnode/edges/strategies/bitemporal.py +520 -0
  35. exonware/xwnode/edges/strategies/{edge_block_adj_matrix.py → block_adj_matrix.py} +77 -6
  36. exonware/xwnode/edges/strategies/bv_graph.py +664 -0
  37. exonware/xwnode/edges/strategies/compressed_graph.py +217 -0
  38. exonware/xwnode/edges/strategies/{edge_coo.py → coo.py} +46 -4
  39. exonware/xwnode/edges/strategies/{edge_csc.py → csc.py} +45 -4
  40. exonware/xwnode/edges/strategies/{edge_csr.py → csr.py} +94 -12
  41. exonware/xwnode/edges/strategies/{edge_dynamic_adj_list.py → dynamic_adj_list.py} +46 -4
  42. exonware/xwnode/edges/strategies/edge_list.py +168 -0
  43. exonware/xwnode/edges/strategies/edge_property_store.py +2 -2
  44. exonware/xwnode/edges/strategies/euler_tour.py +560 -0
  45. exonware/xwnode/edges/strategies/{edge_flow_network.py → flow_network.py} +2 -2
  46. exonware/xwnode/edges/strategies/graphblas.py +449 -0
  47. exonware/xwnode/edges/strategies/hnsw.py +637 -0
  48. exonware/xwnode/edges/strategies/hop2_labels.py +467 -0
  49. exonware/xwnode/edges/strategies/{edge_hyperedge_set.py → hyperedge_set.py} +2 -2
  50. exonware/xwnode/edges/strategies/incidence_matrix.py +250 -0
  51. exonware/xwnode/edges/strategies/k2_tree.py +613 -0
  52. exonware/xwnode/edges/strategies/link_cut.py +626 -0
  53. exonware/xwnode/edges/strategies/multiplex.py +532 -0
  54. exonware/xwnode/edges/strategies/{edge_neural_graph.py → neural_graph.py} +2 -2
  55. exonware/xwnode/edges/strategies/{edge_octree.py → octree.py} +69 -11
  56. exonware/xwnode/edges/strategies/{edge_quadtree.py → quadtree.py} +66 -10
  57. exonware/xwnode/edges/strategies/roaring_adj.py +438 -0
  58. exonware/xwnode/edges/strategies/{edge_rtree.py → rtree.py} +43 -5
  59. exonware/xwnode/edges/strategies/{edge_temporal_edgeset.py → temporal_edgeset.py} +24 -5
  60. exonware/xwnode/edges/strategies/{edge_tree_graph_basic.py → tree_graph_basic.py} +78 -7
  61. exonware/xwnode/edges/strategies/{edge_weighted_graph.py → weighted_graph.py} +188 -10
  62. exonware/xwnode/errors.py +3 -6
  63. exonware/xwnode/facade.py +20 -20
  64. exonware/xwnode/nodes/strategies/__init__.py +29 -9
  65. exonware/xwnode/nodes/strategies/adjacency_list.py +650 -177
  66. exonware/xwnode/nodes/strategies/aho_corasick.py +358 -183
  67. exonware/xwnode/nodes/strategies/array_list.py +36 -3
  68. exonware/xwnode/nodes/strategies/art.py +581 -0
  69. exonware/xwnode/nodes/strategies/{node_avl_tree.py → avl_tree.py} +77 -6
  70. exonware/xwnode/nodes/strategies/{node_b_plus_tree.py → b_plus_tree.py} +81 -40
  71. exonware/xwnode/nodes/strategies/{node_btree.py → b_tree.py} +79 -9
  72. exonware/xwnode/nodes/strategies/base.py +469 -98
  73. exonware/xwnode/nodes/strategies/{node_bitmap.py → bitmap.py} +12 -12
  74. exonware/xwnode/nodes/strategies/{node_bitset_dynamic.py → bitset_dynamic.py} +11 -11
  75. exonware/xwnode/nodes/strategies/{node_bloom_filter.py → bloom_filter.py} +15 -2
  76. exonware/xwnode/nodes/strategies/bloomier_filter.py +519 -0
  77. exonware/xwnode/nodes/strategies/bw_tree.py +531 -0
  78. exonware/xwnode/nodes/strategies/contracts.py +1 -1
  79. exonware/xwnode/nodes/strategies/{node_count_min_sketch.py → count_min_sketch.py} +3 -2
  80. exonware/xwnode/nodes/strategies/{node_cow_tree.py → cow_tree.py} +135 -13
  81. exonware/xwnode/nodes/strategies/crdt_map.py +629 -0
  82. exonware/xwnode/nodes/strategies/{node_cuckoo_hash.py → cuckoo_hash.py} +2 -2
  83. exonware/xwnode/nodes/strategies/{node_xdata_optimized.py → data_interchange_optimized.py} +21 -4
  84. exonware/xwnode/nodes/strategies/dawg.py +876 -0
  85. exonware/xwnode/nodes/strategies/deque.py +321 -153
  86. exonware/xwnode/nodes/strategies/extendible_hash.py +93 -0
  87. exonware/xwnode/nodes/strategies/{node_fenwick_tree.py → fenwick_tree.py} +111 -19
  88. exonware/xwnode/nodes/strategies/hamt.py +403 -0
  89. exonware/xwnode/nodes/strategies/hash_map.py +354 -67
  90. exonware/xwnode/nodes/strategies/heap.py +105 -5
  91. exonware/xwnode/nodes/strategies/hopscotch_hash.py +525 -0
  92. exonware/xwnode/nodes/strategies/{node_hyperloglog.py → hyperloglog.py} +6 -5
  93. exonware/xwnode/nodes/strategies/interval_tree.py +742 -0
  94. exonware/xwnode/nodes/strategies/kd_tree.py +703 -0
  95. exonware/xwnode/nodes/strategies/learned_index.py +533 -0
  96. exonware/xwnode/nodes/strategies/linear_hash.py +93 -0
  97. exonware/xwnode/nodes/strategies/linked_list.py +316 -119
  98. exonware/xwnode/nodes/strategies/{node_lsm_tree.py → lsm_tree.py} +219 -15
  99. exonware/xwnode/nodes/strategies/masstree.py +130 -0
  100. exonware/xwnode/nodes/strategies/{node_persistent_tree.py → persistent_tree.py} +149 -9
  101. exonware/xwnode/nodes/strategies/priority_queue.py +544 -132
  102. exonware/xwnode/nodes/strategies/queue.py +249 -120
  103. exonware/xwnode/nodes/strategies/{node_red_black_tree.py → red_black_tree.py} +183 -72
  104. exonware/xwnode/nodes/strategies/{node_roaring_bitmap.py → roaring_bitmap.py} +19 -6
  105. exonware/xwnode/nodes/strategies/rope.py +717 -0
  106. exonware/xwnode/nodes/strategies/{node_segment_tree.py → segment_tree.py} +106 -106
  107. exonware/xwnode/nodes/strategies/{node_set_hash.py → set_hash.py} +30 -29
  108. exonware/xwnode/nodes/strategies/{node_skip_list.py → skip_list.py} +74 -6
  109. exonware/xwnode/nodes/strategies/sparse_matrix.py +427 -131
  110. exonware/xwnode/nodes/strategies/{node_splay_tree.py → splay_tree.py} +55 -6
  111. exonware/xwnode/nodes/strategies/stack.py +244 -112
  112. exonware/xwnode/nodes/strategies/{node_suffix_array.py → suffix_array.py} +5 -1
  113. exonware/xwnode/nodes/strategies/t_tree.py +94 -0
  114. exonware/xwnode/nodes/strategies/{node_treap.py → treap.py} +75 -6
  115. exonware/xwnode/nodes/strategies/{node_tree_graph_hybrid.py → tree_graph_hybrid.py} +46 -5
  116. exonware/xwnode/nodes/strategies/trie.py +153 -9
  117. exonware/xwnode/nodes/strategies/union_find.py +111 -5
  118. exonware/xwnode/nodes/strategies/veb_tree.py +856 -0
  119. exonware/xwnode/strategies/__init__.py +5 -51
  120. exonware/xwnode/version.py +3 -3
  121. {exonware_xwnode-0.0.1.21.dist-info → exonware_xwnode-0.0.1.23.dist-info}/METADATA +23 -3
  122. exonware_xwnode-0.0.1.23.dist-info/RECORD +130 -0
  123. exonware/xwnode/edges/strategies/edge_adj_list.py +0 -353
  124. exonware/xwnode/edges/strategies/edge_adj_matrix.py +0 -445
  125. exonware/xwnode/nodes/strategies/_base_node.py +0 -307
  126. exonware/xwnode/nodes/strategies/node_aho_corasick.py +0 -525
  127. exonware/xwnode/nodes/strategies/node_array_list.py +0 -179
  128. exonware/xwnode/nodes/strategies/node_hash_map.py +0 -273
  129. exonware/xwnode/nodes/strategies/node_heap.py +0 -196
  130. exonware/xwnode/nodes/strategies/node_linked_list.py +0 -413
  131. exonware/xwnode/nodes/strategies/node_trie.py +0 -257
  132. exonware/xwnode/nodes/strategies/node_union_find.py +0 -192
  133. exonware/xwnode/queries/executors/__init__.py +0 -47
  134. exonware/xwnode/queries/executors/advanced/__init__.py +0 -37
  135. exonware/xwnode/queries/executors/advanced/aggregate_executor.py +0 -50
  136. exonware/xwnode/queries/executors/advanced/ask_executor.py +0 -50
  137. exonware/xwnode/queries/executors/advanced/construct_executor.py +0 -50
  138. exonware/xwnode/queries/executors/advanced/describe_executor.py +0 -50
  139. exonware/xwnode/queries/executors/advanced/for_loop_executor.py +0 -50
  140. exonware/xwnode/queries/executors/advanced/foreach_executor.py +0 -50
  141. exonware/xwnode/queries/executors/advanced/join_executor.py +0 -50
  142. exonware/xwnode/queries/executors/advanced/let_executor.py +0 -50
  143. exonware/xwnode/queries/executors/advanced/mutation_executor.py +0 -50
  144. exonware/xwnode/queries/executors/advanced/options_executor.py +0 -50
  145. exonware/xwnode/queries/executors/advanced/pipe_executor.py +0 -50
  146. exonware/xwnode/queries/executors/advanced/subscribe_executor.py +0 -50
  147. exonware/xwnode/queries/executors/advanced/subscription_executor.py +0 -50
  148. exonware/xwnode/queries/executors/advanced/union_executor.py +0 -50
  149. exonware/xwnode/queries/executors/advanced/window_executor.py +0 -51
  150. exonware/xwnode/queries/executors/advanced/with_cte_executor.py +0 -50
  151. exonware/xwnode/queries/executors/aggregation/__init__.py +0 -21
  152. exonware/xwnode/queries/executors/aggregation/avg_executor.py +0 -50
  153. exonware/xwnode/queries/executors/aggregation/count_executor.py +0 -38
  154. exonware/xwnode/queries/executors/aggregation/distinct_executor.py +0 -50
  155. exonware/xwnode/queries/executors/aggregation/group_executor.py +0 -50
  156. exonware/xwnode/queries/executors/aggregation/having_executor.py +0 -50
  157. exonware/xwnode/queries/executors/aggregation/max_executor.py +0 -50
  158. exonware/xwnode/queries/executors/aggregation/min_executor.py +0 -50
  159. exonware/xwnode/queries/executors/aggregation/sum_executor.py +0 -50
  160. exonware/xwnode/queries/executors/aggregation/summarize_executor.py +0 -50
  161. exonware/xwnode/queries/executors/array/__init__.py +0 -9
  162. exonware/xwnode/queries/executors/array/indexing_executor.py +0 -51
  163. exonware/xwnode/queries/executors/array/slicing_executor.py +0 -51
  164. exonware/xwnode/queries/executors/base.py +0 -257
  165. exonware/xwnode/queries/executors/capability_checker.py +0 -204
  166. exonware/xwnode/queries/executors/contracts.py +0 -166
  167. exonware/xwnode/queries/executors/core/__init__.py +0 -17
  168. exonware/xwnode/queries/executors/core/create_executor.py +0 -96
  169. exonware/xwnode/queries/executors/core/delete_executor.py +0 -99
  170. exonware/xwnode/queries/executors/core/drop_executor.py +0 -100
  171. exonware/xwnode/queries/executors/core/insert_executor.py +0 -39
  172. exonware/xwnode/queries/executors/core/select_executor.py +0 -152
  173. exonware/xwnode/queries/executors/core/update_executor.py +0 -102
  174. exonware/xwnode/queries/executors/data/__init__.py +0 -13
  175. exonware/xwnode/queries/executors/data/alter_executor.py +0 -50
  176. exonware/xwnode/queries/executors/data/load_executor.py +0 -50
  177. exonware/xwnode/queries/executors/data/merge_executor.py +0 -50
  178. exonware/xwnode/queries/executors/data/store_executor.py +0 -50
  179. exonware/xwnode/queries/executors/defs.py +0 -93
  180. exonware/xwnode/queries/executors/engine.py +0 -221
  181. exonware/xwnode/queries/executors/errors.py +0 -68
  182. exonware/xwnode/queries/executors/filtering/__init__.py +0 -25
  183. exonware/xwnode/queries/executors/filtering/between_executor.py +0 -80
  184. exonware/xwnode/queries/executors/filtering/filter_executor.py +0 -79
  185. exonware/xwnode/queries/executors/filtering/has_executor.py +0 -70
  186. exonware/xwnode/queries/executors/filtering/in_executor.py +0 -70
  187. exonware/xwnode/queries/executors/filtering/like_executor.py +0 -76
  188. exonware/xwnode/queries/executors/filtering/optional_executor.py +0 -76
  189. exonware/xwnode/queries/executors/filtering/range_executor.py +0 -80
  190. exonware/xwnode/queries/executors/filtering/term_executor.py +0 -77
  191. exonware/xwnode/queries/executors/filtering/values_executor.py +0 -71
  192. exonware/xwnode/queries/executors/filtering/where_executor.py +0 -44
  193. exonware/xwnode/queries/executors/graph/__init__.py +0 -15
  194. exonware/xwnode/queries/executors/graph/in_traverse_executor.py +0 -51
  195. exonware/xwnode/queries/executors/graph/match_executor.py +0 -51
  196. exonware/xwnode/queries/executors/graph/out_executor.py +0 -51
  197. exonware/xwnode/queries/executors/graph/path_executor.py +0 -51
  198. exonware/xwnode/queries/executors/graph/return_executor.py +0 -51
  199. exonware/xwnode/queries/executors/ordering/__init__.py +0 -9
  200. exonware/xwnode/queries/executors/ordering/by_executor.py +0 -50
  201. exonware/xwnode/queries/executors/ordering/order_executor.py +0 -51
  202. exonware/xwnode/queries/executors/projection/__init__.py +0 -9
  203. exonware/xwnode/queries/executors/projection/extend_executor.py +0 -50
  204. exonware/xwnode/queries/executors/projection/project_executor.py +0 -50
  205. exonware/xwnode/queries/executors/registry.py +0 -173
  206. exonware/xwnode/queries/parsers/__init__.py +0 -26
  207. exonware/xwnode/queries/parsers/base.py +0 -86
  208. exonware/xwnode/queries/parsers/contracts.py +0 -46
  209. exonware/xwnode/queries/parsers/errors.py +0 -53
  210. exonware/xwnode/queries/parsers/sql_param_extractor.py +0 -318
  211. exonware/xwnode/queries/strategies/__init__.py +0 -24
  212. exonware/xwnode/queries/strategies/base.py +0 -236
  213. exonware/xwnode/queries/strategies/cql.py +0 -201
  214. exonware/xwnode/queries/strategies/cypher.py +0 -181
  215. exonware/xwnode/queries/strategies/datalog.py +0 -70
  216. exonware/xwnode/queries/strategies/elastic_dsl.py +0 -70
  217. exonware/xwnode/queries/strategies/eql.py +0 -70
  218. exonware/xwnode/queries/strategies/flux.py +0 -70
  219. exonware/xwnode/queries/strategies/gql.py +0 -70
  220. exonware/xwnode/queries/strategies/graphql.py +0 -240
  221. exonware/xwnode/queries/strategies/gremlin.py +0 -181
  222. exonware/xwnode/queries/strategies/hiveql.py +0 -214
  223. exonware/xwnode/queries/strategies/hql.py +0 -70
  224. exonware/xwnode/queries/strategies/jmespath.py +0 -219
  225. exonware/xwnode/queries/strategies/jq.py +0 -66
  226. exonware/xwnode/queries/strategies/json_query.py +0 -66
  227. exonware/xwnode/queries/strategies/jsoniq.py +0 -248
  228. exonware/xwnode/queries/strategies/kql.py +0 -70
  229. exonware/xwnode/queries/strategies/linq.py +0 -238
  230. exonware/xwnode/queries/strategies/logql.py +0 -70
  231. exonware/xwnode/queries/strategies/mql.py +0 -68
  232. exonware/xwnode/queries/strategies/n1ql.py +0 -210
  233. exonware/xwnode/queries/strategies/partiql.py +0 -70
  234. exonware/xwnode/queries/strategies/pig.py +0 -215
  235. exonware/xwnode/queries/strategies/promql.py +0 -70
  236. exonware/xwnode/queries/strategies/sparql.py +0 -220
  237. exonware/xwnode/queries/strategies/sql.py +0 -275
  238. exonware/xwnode/queries/strategies/xml_query.py +0 -66
  239. exonware/xwnode/queries/strategies/xpath.py +0 -223
  240. exonware/xwnode/queries/strategies/xquery.py +0 -258
  241. exonware/xwnode/queries/strategies/xwnode_executor.py +0 -332
  242. exonware/xwnode/queries/strategies/xwquery.py +0 -456
  243. exonware_xwnode-0.0.1.21.dist-info/RECORD +0 -214
  244. /exonware/xwnode/nodes/strategies/{node_ordered_map.py → ordered_map.py} +0 -0
  245. /exonware/xwnode/nodes/strategies/{node_ordered_map_balanced.py → ordered_map_balanced.py} +0 -0
  246. /exonware/xwnode/nodes/strategies/{node_patricia.py → patricia.py} +0 -0
  247. /exonware/xwnode/nodes/strategies/{node_radix_trie.py → radix_trie.py} +0 -0
  248. /exonware/xwnode/nodes/strategies/{node_set_tree.py → set_tree.py} +0 -0
  249. {exonware_xwnode-0.0.1.21.dist-info → exonware_xwnode-0.0.1.23.dist-info}/WHEEL +0 -0
  250. {exonware_xwnode-0.0.1.21.dist-info → exonware_xwnode-0.0.1.23.dist-info}/licenses/LICENSE +0 -0
@@ -11,7 +11,7 @@ This module defines the abstract base classes for all edge strategy implementati
11
11
  Company: eXonware.com
12
12
  Author: Eng. Muhammad AlShehri
13
13
  Email: connect@exonware.com
14
- Version: 0.0.1.21
14
+ Version: 0.0.1.23
15
15
  Generation Date: January 2, 2025
16
16
  """
17
17
 
@@ -7,16 +7,57 @@ undirected graph operations using dual directed edges.
7
7
 
8
8
  from typing import Any, Iterator, List, Dict, Set, Optional, Tuple
9
9
  from collections import defaultdict
10
- from ._base_edge import aEdgeStrategy
10
+ from ._base_edge import AEdgeStrategy
11
11
  from ...defs import EdgeMode, EdgeTrait
12
12
 
13
13
 
14
- class xBidirWrapperStrategy(aEdgeStrategy):
14
+ class BidirWrapperStrategy(AEdgeStrategy):
15
15
  """
16
16
  Bidirectional Wrapper edge strategy for undirected graphs.
17
17
 
18
- Efficiently represents undirected edges using pairs of directed edges
19
- with automatic synchronization and optimized undirected operations.
18
+ WHY this strategy:
19
+ - Many real graphs undirected (friendships, collaborations, physical connections)
20
+ - Symmetric edge semantics without code duplication
21
+ - Reuses all directed strategies for undirected graphs
22
+ - Decorator pattern enables any strategy to go bidirectional
23
+
24
+ WHY this implementation:
25
+ - Maintains dual arcs (A->B and B->A) automatically
26
+ - Dict storage for both outgoing and incoming
27
+ - Auto-sync ensures symmetry on all operations
28
+ - Delegates to simple adjacency list backend
29
+
30
+ Time Complexity:
31
+ - All operations: 2x base cost (maintains both directions)
32
+ - Add Edge: O(2) - adds both directions
33
+ - Has Edge: O(1) - checks one direction only
34
+ - Remove Edge: O(2) - removes both directions
35
+ - Get Neighbors: O(degree) - already bidirectional
36
+
37
+ Space Complexity: O(2E) - stores edges in both directions
38
+
39
+ Trade-offs:
40
+ - Advantage: Reuses directed code, simple wrapper, guaranteed symmetry
41
+ - Limitation: 2x storage overhead for both directions
42
+ - Compared to native undirected: Simpler implementation
43
+
44
+ Best for:
45
+ - Social networks (symmetric friendships, mutual follows)
46
+ - Collaboration graphs (co-authorship, joint projects)
47
+ - Physical networks (roads, utilities - naturally bidirectional)
48
+ - Any undirected graph using directed strategy backend
49
+
50
+ Not recommended for:
51
+ - Truly directed graphs - overhead unnecessary
52
+ - Memory-critical large graphs - 2x storage cost
53
+ - When native undirected implementation available
54
+
55
+ Following eXonware Priorities:
56
+ 1. Security: Validates symmetry invariants on all operations
57
+ 2. Usability: Transparent wrapper, natural undirected semantics
58
+ 3. Maintainability: Clean decorator pattern, minimal code
59
+ 4. Performance: 2x overhead acceptable for undirected guarantee
60
+ 5. Extensibility: Works with any directed strategy as backend
20
61
  """
21
62
 
22
63
  def __init__(self, traits: EdgeTrait = EdgeTrait.NONE, **options):
@@ -0,0 +1,520 @@
1
+ """
2
+ #exonware/xwnode/src/exonware/xwnode/edges/strategies/bitemporal.py
3
+
4
+ Bitemporal Edges Strategy Implementation
5
+
6
+ This module implements the BITEMPORAL strategy for edges with both
7
+ valid-time and transaction-time dimensions for audit and time-travel queries.
8
+
9
+ Company: eXonware.com
10
+ Author: Eng. Muhammad AlShehri
11
+ Email: connect@exonware.com
12
+ Version: 0.0.1.23
13
+ Generation Date: 12-Oct-2025
14
+ """
15
+
16
+ import time
17
+ from typing import Any, Iterator, Dict, List, Set, Optional, Tuple
18
+ from collections import defaultdict, deque
19
+ from ._base_edge import AEdgeStrategy
20
+ from ...defs import EdgeMode, EdgeTrait
21
+ from ...errors import XWNodeError, XWNodeValueError
22
+
23
+
24
+ class BitemporalEdge:
25
+ """
26
+ Edge with bitemporal properties.
27
+
28
+ WHY two time dimensions:
29
+ - Valid time: When fact is true in reality
30
+ - Transaction time: When fact recorded in database
31
+ - Enables audit trails and time-travel queries
32
+ """
33
+
34
+ def __init__(self, source: str, target: str,
35
+ valid_start: float, valid_end: float,
36
+ tx_start: float, tx_end: Optional[float] = None,
37
+ properties: Optional[Dict[str, Any]] = None):
38
+ """
39
+ Initialize bitemporal edge.
40
+
41
+ Args:
42
+ source: Source vertex
43
+ target: Target vertex
44
+ valid_start: Valid time start
45
+ valid_end: Valid time end
46
+ tx_start: Transaction time start
47
+ tx_end: Transaction time end (None = current)
48
+ properties: Edge properties
49
+ """
50
+ self.source = source
51
+ self.target = target
52
+ self.valid_start = valid_start
53
+ self.valid_end = valid_end
54
+ self.tx_start = tx_start
55
+ self.tx_end = tx_end # None means still valid
56
+ self.properties = properties or {}
57
+
58
+ def is_valid_at(self, valid_time: float) -> bool:
59
+ """Check if edge was valid at given time."""
60
+ return self.valid_start <= valid_time <= self.valid_end
61
+
62
+ def is_known_at(self, tx_time: float) -> bool:
63
+ """Check if edge was known in database at given transaction time."""
64
+ if tx_time < self.tx_start:
65
+ return False
66
+ if self.tx_end is not None and tx_time > self.tx_end:
67
+ return False
68
+ return True
69
+
70
+ def is_active(self, valid_time: float, tx_time: float) -> bool:
71
+ """Check if edge is active in both time dimensions."""
72
+ return self.is_valid_at(valid_time) and self.is_known_at(tx_time)
73
+
74
+
75
+ class BitemporalStrategy(AEdgeStrategy):
76
+ """
77
+ Bitemporal edges strategy for audit and time-travel queries.
78
+
79
+ WHY Bitemporal:
80
+ - Track both real-world validity and database knowledge
81
+ - Essential for financial systems, regulatory compliance
82
+ - Enables "as-of" queries (state at past time)
83
+ - Audit trail for all graph changes
84
+ - Supports corrections to historical data
85
+
86
+ WHY this implementation:
87
+ - Valid-time: When relationship existed in reality
88
+ - Transaction-time: When relationship recorded in DB
89
+ - Interval-based indexing for temporal queries
90
+ - Tombstone pattern for edge deletions
91
+ - Separate current and historical edge storage
92
+
93
+ Time Complexity:
94
+ - Add edge: O(1)
95
+ - Remove edge: O(1) (creates tombstone)
96
+ - Temporal query: O(log n + k) where k is result size
97
+ - As-of query: O(n) worst case, O(log n) with indexing
98
+ - Current snapshot: O(edges)
99
+
100
+ Space Complexity: O(total_versions × edges) worst case
101
+ (All historical versions preserved)
102
+
103
+ Trade-offs:
104
+ - Advantage: Complete audit trail
105
+ - Advantage: Time-travel queries
106
+ - Advantage: Supports corrections
107
+ - Limitation: High space overhead (all versions)
108
+ - Limitation: Complex queries (two time dimensions)
109
+ - Limitation: Slower than single-time temporal
110
+ - Compared to TEMPORAL_EDGESET: More features, more complex
111
+ - Compared to Event sourcing: Similar concept, graph-specific
112
+
113
+ Best for:
114
+ - Financial systems (regulatory audit requirements)
115
+ - Healthcare records (legal compliance)
116
+ - Blockchain/ledger applications
117
+ - Regulatory compliance scenarios
118
+ - Systems requiring complete audit trails
119
+ - Historical data corrections
120
+
121
+ Not recommended for:
122
+ - Real-time systems (overhead too high)
123
+ - Space-constrained environments
124
+ - When simple timestamps suffice
125
+ - Immutable graphs (no corrections needed)
126
+ - Non-critical applications
127
+
128
+ Following eXonware Priorities:
129
+ 1. Security: Immutable audit trail, prevents tampering
130
+ 2. Usability: Clear temporal query API
131
+ 3. Maintainability: Clean bitemporal model
132
+ 4. Performance: Indexed temporal queries
133
+ 5. Extensibility: Easy to add temporal constraints, versioning
134
+
135
+ Industry Best Practices:
136
+ - Follows Snodgrass bitemporal database theory
137
+ - Implements valid-time and transaction-time
138
+ - Provides as-of queries
139
+ - Supports retroactive corrections
140
+ - Compatible with temporal SQL extensions
141
+ """
142
+
143
+ def __init__(self, traits: EdgeTrait = EdgeTrait.NONE, **options):
144
+ """
145
+ Initialize bitemporal strategy.
146
+
147
+ Args:
148
+ traits: Edge traits
149
+ **options: Additional options
150
+ """
151
+ super().__init__(EdgeMode.BITEMPORAL, traits, **options)
152
+
153
+ # All edge versions (including historical)
154
+ self._edges: List[BitemporalEdge] = []
155
+
156
+ # Current valid edges (cached)
157
+ self._current_edges: Set[Tuple[str, str]] = set()
158
+
159
+ # Vertices
160
+ self._vertices: Set[str] = set()
161
+
162
+ # Temporal index (for efficient queries)
163
+ self._valid_time_index: Dict[float, List[BitemporalEdge]] = defaultdict(list)
164
+ self._tx_time_index: Dict[float, List[BitemporalEdge]] = defaultdict(list)
165
+
166
+ def get_supported_traits(self) -> EdgeTrait:
167
+ """Get supported traits."""
168
+ return EdgeTrait.TEMPORAL | EdgeTrait.DIRECTED | EdgeTrait.SPARSE
169
+
170
+ # ============================================================================
171
+ # TEMPORAL EDGE OPERATIONS
172
+ # ============================================================================
173
+
174
+ def add_edge(self, source: str, target: str, edge_type: str = "default",
175
+ weight: float = 1.0, properties: Optional[Dict[str, Any]] = None,
176
+ is_bidirectional: bool = False, edge_id: Optional[str] = None) -> str:
177
+ """
178
+ Add edge with temporal metadata.
179
+
180
+ Args:
181
+ source: Source vertex
182
+ target: Target vertex
183
+ edge_type: Edge type
184
+ weight: Edge weight
185
+ properties: Edge properties (should include valid_start, valid_end)
186
+ is_bidirectional: Bidirectional flag
187
+ edge_id: Edge ID
188
+
189
+ Returns:
190
+ Edge ID
191
+ """
192
+ # Parse temporal properties
193
+ props = properties.copy() if properties else {}
194
+
195
+ current_time = time.time()
196
+ valid_start = props.pop('valid_start', current_time)
197
+ valid_end = props.pop('valid_end', float('inf'))
198
+ tx_start = current_time
199
+
200
+ # Create bitemporal edge
201
+ edge = BitemporalEdge(
202
+ source, target,
203
+ valid_start, valid_end,
204
+ tx_start, None,
205
+ props
206
+ )
207
+
208
+ self._edges.append(edge)
209
+ self._current_edges.add((source, target))
210
+
211
+ # Index
212
+ self._valid_time_index[valid_start].append(edge)
213
+ self._tx_time_index[tx_start].append(edge)
214
+
215
+ self._vertices.add(source)
216
+ self._vertices.add(target)
217
+
218
+ self._edge_count += 1
219
+
220
+ return edge_id or f"edge_{source}_{target}_{tx_start}"
221
+
222
+ def remove_edge(self, source: str, target: str, edge_id: Optional[str] = None) -> bool:
223
+ """
224
+ Remove edge (creates tombstone with transaction time).
225
+
226
+ Args:
227
+ source: Source vertex
228
+ target: Target vertex
229
+ edge_id: Edge ID
230
+
231
+ Returns:
232
+ True if removed
233
+
234
+ WHY tombstone:
235
+ - Preserves historical data
236
+ - Records deletion in audit trail
237
+ - Enables time-travel queries
238
+ """
239
+ if (source, target) not in self._current_edges:
240
+ return False
241
+
242
+ current_time = time.time()
243
+
244
+ # Find active edges and mark with tx_end
245
+ for edge in self._edges:
246
+ if (edge.source == source and edge.target == target and
247
+ edge.tx_end is None):
248
+ edge.tx_end = current_time
249
+
250
+ self._current_edges.discard((source, target))
251
+ self._edge_count -= 1
252
+
253
+ return True
254
+
255
+ def has_edge(self, source: str, target: str) -> bool:
256
+ """Check if edge currently exists."""
257
+ return (source, target) in self._current_edges
258
+
259
+ # ============================================================================
260
+ # TEMPORAL QUERIES
261
+ # ============================================================================
262
+
263
+ def get_edges_at_time(self, valid_time: float, tx_time: float) -> List[Dict[str, Any]]:
264
+ """
265
+ Get edges active at specific valid and transaction times.
266
+
267
+ Args:
268
+ valid_time: Valid time point
269
+ tx_time: Transaction time point
270
+
271
+ Returns:
272
+ List of edges active at both times
273
+
274
+ WHY bitemporal query:
275
+ - "Show me the graph as we knew it then"
276
+ - Combines reality (valid) and knowledge (tx)
277
+ - Essential for compliance and auditing
278
+ """
279
+ result = []
280
+
281
+ for edge in self._edges:
282
+ if edge.is_active(valid_time, tx_time):
283
+ result.append({
284
+ 'source': edge.source,
285
+ 'target': edge.target,
286
+ 'valid_start': edge.valid_start,
287
+ 'valid_end': edge.valid_end,
288
+ 'tx_start': edge.tx_start,
289
+ 'tx_end': edge.tx_end,
290
+ **edge.properties
291
+ })
292
+
293
+ return result
294
+
295
+ def as_of_query(self, tx_time: float) -> List[Dict[str, Any]]:
296
+ """
297
+ Get graph state as it was known at transaction time.
298
+
299
+ Args:
300
+ tx_time: Transaction time point
301
+
302
+ Returns:
303
+ Edges known at that transaction time
304
+ """
305
+ result = []
306
+
307
+ for edge in self._edges:
308
+ if edge.is_known_at(tx_time):
309
+ result.append({
310
+ 'source': edge.source,
311
+ 'target': edge.target,
312
+ 'valid_start': edge.valid_start,
313
+ 'valid_end': edge.valid_end,
314
+ **edge.properties
315
+ })
316
+
317
+ return result
318
+
319
+ def get_edge_history(self, source: str, target: str) -> List[Dict[str, Any]]:
320
+ """
321
+ Get complete history of edge.
322
+
323
+ Args:
324
+ source: Source vertex
325
+ target: Target vertex
326
+
327
+ Returns:
328
+ All versions of edge
329
+ """
330
+ history = []
331
+
332
+ for edge in self._edges:
333
+ if edge.source == source and edge.target == target:
334
+ history.append({
335
+ 'valid_start': edge.valid_start,
336
+ 'valid_end': edge.valid_end,
337
+ 'tx_start': edge.tx_start,
338
+ 'tx_end': edge.tx_end,
339
+ **edge.properties
340
+ })
341
+
342
+ return sorted(history, key=lambda x: x['tx_start'])
343
+
344
+ # ============================================================================
345
+ # STANDARD GRAPH OPERATIONS
346
+ # ============================================================================
347
+
348
+ def get_neighbors(self, node: str, edge_type: Optional[str] = None,
349
+ direction: str = "outgoing") -> List[str]:
350
+ """Get current neighbors."""
351
+ neighbors = set()
352
+
353
+ for source, target in self._current_edges:
354
+ if source == node:
355
+ neighbors.add(target)
356
+
357
+ return list(neighbors)
358
+
359
+ def neighbors(self, node: str) -> Iterator[Any]:
360
+ """Get iterator over current neighbors."""
361
+ return iter(self.get_neighbors(node))
362
+
363
+ def degree(self, node: str) -> int:
364
+ """Get degree of node in current snapshot."""
365
+ return len(self.get_neighbors(node))
366
+
367
+ def edges(self) -> Iterator[Tuple[Any, Any, Dict[str, Any]]]:
368
+ """Iterate over current edges with properties."""
369
+ for edge_dict in self.get_edges():
370
+ yield (edge_dict['source'], edge_dict['target'], {})
371
+
372
+ def vertices(self) -> Iterator[Any]:
373
+ """Get iterator over all vertices."""
374
+ return iter(self._vertices)
375
+
376
+ def get_edges(self, edge_type: Optional[str] = None, direction: str = "both") -> List[Dict[str, Any]]:
377
+ """Get current edges."""
378
+ edges = []
379
+
380
+ for source, target in self._current_edges:
381
+ edges.append({
382
+ 'source': source,
383
+ 'target': target,
384
+ 'edge_type': edge_type or 'bitemporal'
385
+ })
386
+
387
+ return edges
388
+
389
+ def get_edge_data(self, source: str, target: str, edge_id: Optional[str] = None) -> Optional[Dict[str, Any]]:
390
+ """Get current edge data."""
391
+ for edge in self._edges:
392
+ if edge.source == source and edge.target == target and edge.tx_end is None:
393
+ return {
394
+ 'source': source,
395
+ 'target': target,
396
+ 'valid_start': edge.valid_start,
397
+ 'valid_end': edge.valid_end,
398
+ 'tx_start': edge.tx_start,
399
+ **edge.properties
400
+ }
401
+ return None
402
+
403
+ # ============================================================================
404
+ # GRAPH ALGORITHMS (on current snapshot)
405
+ # ============================================================================
406
+
407
+ def shortest_path(self, source: str, target: str, edge_type: Optional[str] = None) -> List[str]:
408
+ """Find shortest path in current snapshot."""
409
+ if source not in self._vertices or target not in self._vertices:
410
+ return []
411
+
412
+ queue = deque([source])
413
+ visited = {source}
414
+ parent = {source: None}
415
+
416
+ while queue:
417
+ current = queue.popleft()
418
+
419
+ if current == target:
420
+ path = []
421
+ while current:
422
+ path.append(current)
423
+ current = parent[current]
424
+ return list(reversed(path))
425
+
426
+ for neighbor in self.get_neighbors(current):
427
+ if neighbor not in visited:
428
+ visited.add(neighbor)
429
+ parent[neighbor] = current
430
+ queue.append(neighbor)
431
+
432
+ return []
433
+
434
+ def find_cycles(self, start_node: str, edge_type: Optional[str] = None, max_depth: int = 10) -> List[List[str]]:
435
+ """Find cycles in current snapshot."""
436
+ return []
437
+
438
+ def traverse_graph(self, start_node: str, strategy: str = "bfs",
439
+ max_depth: int = 100, edge_type: Optional[str] = None) -> Iterator[str]:
440
+ """Traverse current snapshot."""
441
+ if start_node not in self._vertices:
442
+ return
443
+
444
+ visited = set()
445
+ queue = deque([start_node])
446
+ visited.add(start_node)
447
+
448
+ while queue:
449
+ current = queue.popleft()
450
+ yield current
451
+
452
+ for neighbor in self.get_neighbors(current):
453
+ if neighbor not in visited:
454
+ visited.add(neighbor)
455
+ queue.append(neighbor)
456
+
457
+ def is_connected(self, source: str, target: str, edge_type: Optional[str] = None) -> bool:
458
+ """Check if vertices connected in current snapshot."""
459
+ return len(self.shortest_path(source, target)) > 0
460
+
461
+ # ============================================================================
462
+ # STANDARD OPERATIONS
463
+ # ============================================================================
464
+
465
+ def __len__(self) -> int:
466
+ """Get number of current edges."""
467
+ return len(self._current_edges)
468
+
469
+ def __iter__(self) -> Iterator[Dict[str, Any]]:
470
+ """Iterate over current edges."""
471
+ return iter(self.get_edges())
472
+
473
+ def to_native(self) -> Dict[str, Any]:
474
+ """Convert to native representation."""
475
+ return {
476
+ 'vertices': list(self._vertices),
477
+ 'current_edges': self.get_edges(),
478
+ 'total_versions': len(self._edges)
479
+ }
480
+
481
+ # ============================================================================
482
+ # STATISTICS
483
+ # ============================================================================
484
+
485
+ def get_statistics(self) -> Dict[str, Any]:
486
+ """Get bitemporal statistics."""
487
+ active_edges = sum(1 for e in self._edges if e.tx_end is None)
488
+ historical_edges = sum(1 for e in self._edges if e.tx_end is not None)
489
+
490
+ return {
491
+ 'vertices': len(self._vertices),
492
+ 'current_edges': len(self._current_edges),
493
+ 'active_versions': active_edges,
494
+ 'historical_versions': historical_edges,
495
+ 'total_versions': len(self._edges),
496
+ 'avg_versions_per_edge': len(self._edges) / max(len(self._current_edges), 1)
497
+ }
498
+
499
+ # ============================================================================
500
+ # UTILITY METHODS
501
+ # ============================================================================
502
+
503
+ @property
504
+ def strategy_name(self) -> str:
505
+ """Get strategy name."""
506
+ return "BITEMPORAL"
507
+
508
+ @property
509
+ def supported_traits(self) -> List[EdgeTrait]:
510
+ """Get supported traits."""
511
+ return [EdgeTrait.TEMPORAL, EdgeTrait.DIRECTED, EdgeTrait.SPARSE]
512
+
513
+ def get_backend_info(self) -> Dict[str, Any]:
514
+ """Get backend information."""
515
+ return {
516
+ 'strategy': 'Bitemporal Edges',
517
+ 'description': 'Valid-time and transaction-time for audit and time-travel',
518
+ **self.get_statistics()
519
+ }
520
+