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.
- exonware/__init__.py +8 -1
- exonware/xwnode/__init__.py +18 -5
- exonware/xwnode/add_strategy_types.py +165 -0
- exonware/xwnode/base.py +7 -5
- exonware/xwnode/common/__init__.py +1 -1
- exonware/xwnode/common/graph/__init__.py +30 -0
- exonware/xwnode/common/graph/caching.py +131 -0
- exonware/xwnode/common/graph/contracts.py +100 -0
- exonware/xwnode/common/graph/errors.py +44 -0
- exonware/xwnode/common/graph/indexing.py +260 -0
- exonware/xwnode/common/graph/manager.py +568 -0
- exonware/xwnode/common/management/__init__.py +3 -5
- exonware/xwnode/common/management/manager.py +9 -9
- exonware/xwnode/common/management/migration.py +6 -6
- exonware/xwnode/common/monitoring/__init__.py +3 -5
- exonware/xwnode/common/monitoring/metrics.py +7 -3
- exonware/xwnode/common/monitoring/pattern_detector.py +2 -2
- exonware/xwnode/common/monitoring/performance_monitor.py +6 -2
- exonware/xwnode/common/patterns/__init__.py +3 -5
- exonware/xwnode/common/patterns/advisor.py +1 -1
- exonware/xwnode/common/patterns/flyweight.py +6 -2
- exonware/xwnode/common/patterns/registry.py +203 -184
- exonware/xwnode/common/utils/__init__.py +25 -11
- exonware/xwnode/common/utils/simple.py +1 -1
- exonware/xwnode/config.py +3 -8
- exonware/xwnode/contracts.py +4 -105
- exonware/xwnode/defs.py +413 -159
- exonware/xwnode/edges/strategies/__init__.py +86 -4
- exonware/xwnode/edges/strategies/_base_edge.py +2 -2
- exonware/xwnode/edges/strategies/adj_list.py +287 -121
- exonware/xwnode/edges/strategies/adj_matrix.py +316 -222
- exonware/xwnode/edges/strategies/base.py +1 -1
- exonware/xwnode/edges/strategies/{edge_bidir_wrapper.py → bidir_wrapper.py} +45 -4
- exonware/xwnode/edges/strategies/bitemporal.py +520 -0
- exonware/xwnode/edges/strategies/{edge_block_adj_matrix.py → block_adj_matrix.py} +77 -6
- exonware/xwnode/edges/strategies/bv_graph.py +664 -0
- exonware/xwnode/edges/strategies/compressed_graph.py +217 -0
- exonware/xwnode/edges/strategies/{edge_coo.py → coo.py} +46 -4
- exonware/xwnode/edges/strategies/{edge_csc.py → csc.py} +45 -4
- exonware/xwnode/edges/strategies/{edge_csr.py → csr.py} +94 -12
- exonware/xwnode/edges/strategies/{edge_dynamic_adj_list.py → dynamic_adj_list.py} +46 -4
- exonware/xwnode/edges/strategies/edge_list.py +168 -0
- exonware/xwnode/edges/strategies/edge_property_store.py +2 -2
- exonware/xwnode/edges/strategies/euler_tour.py +560 -0
- exonware/xwnode/edges/strategies/{edge_flow_network.py → flow_network.py} +2 -2
- exonware/xwnode/edges/strategies/graphblas.py +449 -0
- exonware/xwnode/edges/strategies/hnsw.py +637 -0
- exonware/xwnode/edges/strategies/hop2_labels.py +467 -0
- exonware/xwnode/edges/strategies/{edge_hyperedge_set.py → hyperedge_set.py} +2 -2
- exonware/xwnode/edges/strategies/incidence_matrix.py +250 -0
- exonware/xwnode/edges/strategies/k2_tree.py +613 -0
- exonware/xwnode/edges/strategies/link_cut.py +626 -0
- exonware/xwnode/edges/strategies/multiplex.py +532 -0
- exonware/xwnode/edges/strategies/{edge_neural_graph.py → neural_graph.py} +2 -2
- exonware/xwnode/edges/strategies/{edge_octree.py → octree.py} +69 -11
- exonware/xwnode/edges/strategies/{edge_quadtree.py → quadtree.py} +66 -10
- exonware/xwnode/edges/strategies/roaring_adj.py +438 -0
- exonware/xwnode/edges/strategies/{edge_rtree.py → rtree.py} +43 -5
- exonware/xwnode/edges/strategies/{edge_temporal_edgeset.py → temporal_edgeset.py} +24 -5
- exonware/xwnode/edges/strategies/{edge_tree_graph_basic.py → tree_graph_basic.py} +78 -7
- exonware/xwnode/edges/strategies/{edge_weighted_graph.py → weighted_graph.py} +188 -10
- exonware/xwnode/errors.py +3 -6
- exonware/xwnode/facade.py +20 -20
- exonware/xwnode/nodes/strategies/__init__.py +29 -9
- exonware/xwnode/nodes/strategies/adjacency_list.py +650 -177
- exonware/xwnode/nodes/strategies/aho_corasick.py +358 -183
- exonware/xwnode/nodes/strategies/array_list.py +36 -3
- exonware/xwnode/nodes/strategies/art.py +581 -0
- exonware/xwnode/nodes/strategies/{node_avl_tree.py → avl_tree.py} +77 -6
- exonware/xwnode/nodes/strategies/{node_b_plus_tree.py → b_plus_tree.py} +81 -40
- exonware/xwnode/nodes/strategies/{node_btree.py → b_tree.py} +79 -9
- exonware/xwnode/nodes/strategies/base.py +469 -98
- exonware/xwnode/nodes/strategies/{node_bitmap.py → bitmap.py} +12 -12
- exonware/xwnode/nodes/strategies/{node_bitset_dynamic.py → bitset_dynamic.py} +11 -11
- exonware/xwnode/nodes/strategies/{node_bloom_filter.py → bloom_filter.py} +15 -2
- exonware/xwnode/nodes/strategies/bloomier_filter.py +519 -0
- exonware/xwnode/nodes/strategies/bw_tree.py +531 -0
- exonware/xwnode/nodes/strategies/contracts.py +1 -1
- exonware/xwnode/nodes/strategies/{node_count_min_sketch.py → count_min_sketch.py} +3 -2
- exonware/xwnode/nodes/strategies/{node_cow_tree.py → cow_tree.py} +135 -13
- exonware/xwnode/nodes/strategies/crdt_map.py +629 -0
- exonware/xwnode/nodes/strategies/{node_cuckoo_hash.py → cuckoo_hash.py} +2 -2
- exonware/xwnode/nodes/strategies/{node_xdata_optimized.py → data_interchange_optimized.py} +21 -4
- exonware/xwnode/nodes/strategies/dawg.py +876 -0
- exonware/xwnode/nodes/strategies/deque.py +321 -153
- exonware/xwnode/nodes/strategies/extendible_hash.py +93 -0
- exonware/xwnode/nodes/strategies/{node_fenwick_tree.py → fenwick_tree.py} +111 -19
- exonware/xwnode/nodes/strategies/hamt.py +403 -0
- exonware/xwnode/nodes/strategies/hash_map.py +354 -67
- exonware/xwnode/nodes/strategies/heap.py +105 -5
- exonware/xwnode/nodes/strategies/hopscotch_hash.py +525 -0
- exonware/xwnode/nodes/strategies/{node_hyperloglog.py → hyperloglog.py} +6 -5
- exonware/xwnode/nodes/strategies/interval_tree.py +742 -0
- exonware/xwnode/nodes/strategies/kd_tree.py +703 -0
- exonware/xwnode/nodes/strategies/learned_index.py +533 -0
- exonware/xwnode/nodes/strategies/linear_hash.py +93 -0
- exonware/xwnode/nodes/strategies/linked_list.py +316 -119
- exonware/xwnode/nodes/strategies/{node_lsm_tree.py → lsm_tree.py} +219 -15
- exonware/xwnode/nodes/strategies/masstree.py +130 -0
- exonware/xwnode/nodes/strategies/{node_persistent_tree.py → persistent_tree.py} +149 -9
- exonware/xwnode/nodes/strategies/priority_queue.py +544 -132
- exonware/xwnode/nodes/strategies/queue.py +249 -120
- exonware/xwnode/nodes/strategies/{node_red_black_tree.py → red_black_tree.py} +183 -72
- exonware/xwnode/nodes/strategies/{node_roaring_bitmap.py → roaring_bitmap.py} +19 -6
- exonware/xwnode/nodes/strategies/rope.py +717 -0
- exonware/xwnode/nodes/strategies/{node_segment_tree.py → segment_tree.py} +106 -106
- exonware/xwnode/nodes/strategies/{node_set_hash.py → set_hash.py} +30 -29
- exonware/xwnode/nodes/strategies/{node_skip_list.py → skip_list.py} +74 -6
- exonware/xwnode/nodes/strategies/sparse_matrix.py +427 -131
- exonware/xwnode/nodes/strategies/{node_splay_tree.py → splay_tree.py} +55 -6
- exonware/xwnode/nodes/strategies/stack.py +244 -112
- exonware/xwnode/nodes/strategies/{node_suffix_array.py → suffix_array.py} +5 -1
- exonware/xwnode/nodes/strategies/t_tree.py +94 -0
- exonware/xwnode/nodes/strategies/{node_treap.py → treap.py} +75 -6
- exonware/xwnode/nodes/strategies/{node_tree_graph_hybrid.py → tree_graph_hybrid.py} +46 -5
- exonware/xwnode/nodes/strategies/trie.py +153 -9
- exonware/xwnode/nodes/strategies/union_find.py +111 -5
- exonware/xwnode/nodes/strategies/veb_tree.py +856 -0
- exonware/xwnode/strategies/__init__.py +5 -51
- exonware/xwnode/version.py +3 -3
- {exonware_xwnode-0.0.1.21.dist-info → exonware_xwnode-0.0.1.23.dist-info}/METADATA +23 -3
- exonware_xwnode-0.0.1.23.dist-info/RECORD +130 -0
- exonware/xwnode/edges/strategies/edge_adj_list.py +0 -353
- exonware/xwnode/edges/strategies/edge_adj_matrix.py +0 -445
- exonware/xwnode/nodes/strategies/_base_node.py +0 -307
- exonware/xwnode/nodes/strategies/node_aho_corasick.py +0 -525
- exonware/xwnode/nodes/strategies/node_array_list.py +0 -179
- exonware/xwnode/nodes/strategies/node_hash_map.py +0 -273
- exonware/xwnode/nodes/strategies/node_heap.py +0 -196
- exonware/xwnode/nodes/strategies/node_linked_list.py +0 -413
- exonware/xwnode/nodes/strategies/node_trie.py +0 -257
- exonware/xwnode/nodes/strategies/node_union_find.py +0 -192
- exonware/xwnode/queries/executors/__init__.py +0 -47
- exonware/xwnode/queries/executors/advanced/__init__.py +0 -37
- exonware/xwnode/queries/executors/advanced/aggregate_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/ask_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/construct_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/describe_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/for_loop_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/foreach_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/join_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/let_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/mutation_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/options_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/pipe_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/subscribe_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/subscription_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/union_executor.py +0 -50
- exonware/xwnode/queries/executors/advanced/window_executor.py +0 -51
- exonware/xwnode/queries/executors/advanced/with_cte_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/__init__.py +0 -21
- exonware/xwnode/queries/executors/aggregation/avg_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/count_executor.py +0 -38
- exonware/xwnode/queries/executors/aggregation/distinct_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/group_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/having_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/max_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/min_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/sum_executor.py +0 -50
- exonware/xwnode/queries/executors/aggregation/summarize_executor.py +0 -50
- exonware/xwnode/queries/executors/array/__init__.py +0 -9
- exonware/xwnode/queries/executors/array/indexing_executor.py +0 -51
- exonware/xwnode/queries/executors/array/slicing_executor.py +0 -51
- exonware/xwnode/queries/executors/base.py +0 -257
- exonware/xwnode/queries/executors/capability_checker.py +0 -204
- exonware/xwnode/queries/executors/contracts.py +0 -166
- exonware/xwnode/queries/executors/core/__init__.py +0 -17
- exonware/xwnode/queries/executors/core/create_executor.py +0 -96
- exonware/xwnode/queries/executors/core/delete_executor.py +0 -99
- exonware/xwnode/queries/executors/core/drop_executor.py +0 -100
- exonware/xwnode/queries/executors/core/insert_executor.py +0 -39
- exonware/xwnode/queries/executors/core/select_executor.py +0 -152
- exonware/xwnode/queries/executors/core/update_executor.py +0 -102
- exonware/xwnode/queries/executors/data/__init__.py +0 -13
- exonware/xwnode/queries/executors/data/alter_executor.py +0 -50
- exonware/xwnode/queries/executors/data/load_executor.py +0 -50
- exonware/xwnode/queries/executors/data/merge_executor.py +0 -50
- exonware/xwnode/queries/executors/data/store_executor.py +0 -50
- exonware/xwnode/queries/executors/defs.py +0 -93
- exonware/xwnode/queries/executors/engine.py +0 -221
- exonware/xwnode/queries/executors/errors.py +0 -68
- exonware/xwnode/queries/executors/filtering/__init__.py +0 -25
- exonware/xwnode/queries/executors/filtering/between_executor.py +0 -80
- exonware/xwnode/queries/executors/filtering/filter_executor.py +0 -79
- exonware/xwnode/queries/executors/filtering/has_executor.py +0 -70
- exonware/xwnode/queries/executors/filtering/in_executor.py +0 -70
- exonware/xwnode/queries/executors/filtering/like_executor.py +0 -76
- exonware/xwnode/queries/executors/filtering/optional_executor.py +0 -76
- exonware/xwnode/queries/executors/filtering/range_executor.py +0 -80
- exonware/xwnode/queries/executors/filtering/term_executor.py +0 -77
- exonware/xwnode/queries/executors/filtering/values_executor.py +0 -71
- exonware/xwnode/queries/executors/filtering/where_executor.py +0 -44
- exonware/xwnode/queries/executors/graph/__init__.py +0 -15
- exonware/xwnode/queries/executors/graph/in_traverse_executor.py +0 -51
- exonware/xwnode/queries/executors/graph/match_executor.py +0 -51
- exonware/xwnode/queries/executors/graph/out_executor.py +0 -51
- exonware/xwnode/queries/executors/graph/path_executor.py +0 -51
- exonware/xwnode/queries/executors/graph/return_executor.py +0 -51
- exonware/xwnode/queries/executors/ordering/__init__.py +0 -9
- exonware/xwnode/queries/executors/ordering/by_executor.py +0 -50
- exonware/xwnode/queries/executors/ordering/order_executor.py +0 -51
- exonware/xwnode/queries/executors/projection/__init__.py +0 -9
- exonware/xwnode/queries/executors/projection/extend_executor.py +0 -50
- exonware/xwnode/queries/executors/projection/project_executor.py +0 -50
- exonware/xwnode/queries/executors/registry.py +0 -173
- exonware/xwnode/queries/parsers/__init__.py +0 -26
- exonware/xwnode/queries/parsers/base.py +0 -86
- exonware/xwnode/queries/parsers/contracts.py +0 -46
- exonware/xwnode/queries/parsers/errors.py +0 -53
- exonware/xwnode/queries/parsers/sql_param_extractor.py +0 -318
- exonware/xwnode/queries/strategies/__init__.py +0 -24
- exonware/xwnode/queries/strategies/base.py +0 -236
- exonware/xwnode/queries/strategies/cql.py +0 -201
- exonware/xwnode/queries/strategies/cypher.py +0 -181
- exonware/xwnode/queries/strategies/datalog.py +0 -70
- exonware/xwnode/queries/strategies/elastic_dsl.py +0 -70
- exonware/xwnode/queries/strategies/eql.py +0 -70
- exonware/xwnode/queries/strategies/flux.py +0 -70
- exonware/xwnode/queries/strategies/gql.py +0 -70
- exonware/xwnode/queries/strategies/graphql.py +0 -240
- exonware/xwnode/queries/strategies/gremlin.py +0 -181
- exonware/xwnode/queries/strategies/hiveql.py +0 -214
- exonware/xwnode/queries/strategies/hql.py +0 -70
- exonware/xwnode/queries/strategies/jmespath.py +0 -219
- exonware/xwnode/queries/strategies/jq.py +0 -66
- exonware/xwnode/queries/strategies/json_query.py +0 -66
- exonware/xwnode/queries/strategies/jsoniq.py +0 -248
- exonware/xwnode/queries/strategies/kql.py +0 -70
- exonware/xwnode/queries/strategies/linq.py +0 -238
- exonware/xwnode/queries/strategies/logql.py +0 -70
- exonware/xwnode/queries/strategies/mql.py +0 -68
- exonware/xwnode/queries/strategies/n1ql.py +0 -210
- exonware/xwnode/queries/strategies/partiql.py +0 -70
- exonware/xwnode/queries/strategies/pig.py +0 -215
- exonware/xwnode/queries/strategies/promql.py +0 -70
- exonware/xwnode/queries/strategies/sparql.py +0 -220
- exonware/xwnode/queries/strategies/sql.py +0 -275
- exonware/xwnode/queries/strategies/xml_query.py +0 -66
- exonware/xwnode/queries/strategies/xpath.py +0 -223
- exonware/xwnode/queries/strategies/xquery.py +0 -258
- exonware/xwnode/queries/strategies/xwnode_executor.py +0 -332
- exonware/xwnode/queries/strategies/xwquery.py +0 -456
- exonware_xwnode-0.0.1.21.dist-info/RECORD +0 -214
- /exonware/xwnode/nodes/strategies/{node_ordered_map.py → ordered_map.py} +0 -0
- /exonware/xwnode/nodes/strategies/{node_ordered_map_balanced.py → ordered_map_balanced.py} +0 -0
- /exonware/xwnode/nodes/strategies/{node_patricia.py → patricia.py} +0 -0
- /exonware/xwnode/nodes/strategies/{node_radix_trie.py → radix_trie.py} +0 -0
- /exonware/xwnode/nodes/strategies/{node_set_tree.py → set_tree.py} +0 -0
- {exonware_xwnode-0.0.1.21.dist-info → exonware_xwnode-0.0.1.23.dist-info}/WHEEL +0 -0
- {exonware_xwnode-0.0.1.21.dist-info → exonware_xwnode-0.0.1.23.dist-info}/licenses/LICENSE +0 -0
@@ -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
|
10
|
+
from ._base_edge import AEdgeStrategy
|
11
11
|
from ...defs import EdgeMode, EdgeTrait
|
12
12
|
|
13
13
|
|
14
|
-
class
|
14
|
+
class BidirWrapperStrategy(AEdgeStrategy):
|
15
15
|
"""
|
16
16
|
Bidirectional Wrapper edge strategy for undirected graphs.
|
17
17
|
|
18
|
-
|
19
|
-
|
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
|
+
|