pytrilogy 0.0.2.49__py3-none-any.whl → 0.0.2.51__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.

Potentially problematic release.


This version of pytrilogy might be problematic. Click here for more details.

Files changed (43) hide show
  1. {pytrilogy-0.0.2.49.dist-info → pytrilogy-0.0.2.51.dist-info}/METADATA +1 -1
  2. {pytrilogy-0.0.2.49.dist-info → pytrilogy-0.0.2.51.dist-info}/RECORD +43 -41
  3. trilogy/__init__.py +1 -1
  4. trilogy/core/enums.py +11 -0
  5. trilogy/core/functions.py +4 -1
  6. trilogy/core/internal.py +5 -1
  7. trilogy/core/models.py +135 -263
  8. trilogy/core/processing/concept_strategies_v3.py +14 -7
  9. trilogy/core/processing/node_generators/basic_node.py +7 -3
  10. trilogy/core/processing/node_generators/common.py +8 -5
  11. trilogy/core/processing/node_generators/filter_node.py +5 -8
  12. trilogy/core/processing/node_generators/group_node.py +24 -9
  13. trilogy/core/processing/node_generators/group_to_node.py +0 -2
  14. trilogy/core/processing/node_generators/multiselect_node.py +4 -5
  15. trilogy/core/processing/node_generators/node_merge_node.py +14 -3
  16. trilogy/core/processing/node_generators/rowset_node.py +3 -5
  17. trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
  18. trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +203 -0
  19. trilogy/core/processing/node_generators/select_merge_node.py +153 -66
  20. trilogy/core/processing/node_generators/union_node.py +0 -1
  21. trilogy/core/processing/node_generators/unnest_node.py +0 -2
  22. trilogy/core/processing/node_generators/window_node.py +0 -2
  23. trilogy/core/processing/nodes/base_node.py +2 -36
  24. trilogy/core/processing/nodes/filter_node.py +0 -3
  25. trilogy/core/processing/nodes/group_node.py +19 -13
  26. trilogy/core/processing/nodes/merge_node.py +2 -5
  27. trilogy/core/processing/nodes/select_node_v2.py +0 -4
  28. trilogy/core/processing/nodes/union_node.py +0 -3
  29. trilogy/core/processing/nodes/unnest_node.py +0 -3
  30. trilogy/core/processing/nodes/window_node.py +0 -3
  31. trilogy/core/processing/utility.py +3 -0
  32. trilogy/core/query_processor.py +0 -1
  33. trilogy/dialect/base.py +14 -2
  34. trilogy/dialect/duckdb.py +7 -0
  35. trilogy/hooks/graph_hook.py +17 -1
  36. trilogy/parsing/common.py +68 -17
  37. trilogy/parsing/parse_engine.py +70 -20
  38. trilogy/parsing/render.py +8 -1
  39. trilogy/parsing/trilogy.lark +3 -1
  40. {pytrilogy-0.0.2.49.dist-info → pytrilogy-0.0.2.51.dist-info}/LICENSE.md +0 -0
  41. {pytrilogy-0.0.2.49.dist-info → pytrilogy-0.0.2.51.dist-info}/WHEEL +0 -0
  42. {pytrilogy-0.0.2.49.dist-info → pytrilogy-0.0.2.51.dist-info}/entry_points.txt +0 -0
  43. {pytrilogy-0.0.2.49.dist-info → pytrilogy-0.0.2.51.dist-info}/top_level.txt +0 -0
@@ -359,7 +359,6 @@ def generate_node(
359
359
  input_concepts=[],
360
360
  output_concepts=constant_targets,
361
361
  environment=environment,
362
- g=g,
363
362
  parents=[],
364
363
  depth=depth + 1,
365
364
  )
@@ -450,6 +449,7 @@ def generate_node(
450
449
  conditions=conditions,
451
450
  )
452
451
  if not check:
452
+
453
453
  logger.info(
454
454
  f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Could not resolve root concepts, checking for expanded concepts"
455
455
  )
@@ -471,7 +471,6 @@ def generate_node(
471
471
  x
472
472
  for x in ex_resolve.output_concepts
473
473
  if x.address not in [y.address for y in root_targets]
474
- and x not in ex_resolve.grain.components
475
474
  ]
476
475
 
477
476
  pseudonyms = [
@@ -479,10 +478,19 @@ def generate_node(
479
478
  for x in extra
480
479
  if any(x.address in y.pseudonyms for y in root_targets)
481
480
  ]
482
- # if we're only connected by a pseudonym, keep those in output
483
- expanded.set_output_concepts(root_targets + pseudonyms)
481
+ logger.info(
482
+ f"{depth_to_prefix(depth)}{LOGGER_PREFIX} reducing final outputs, was {[c.address for c in ex_resolve.output_concepts]} with extra {[c.address for c in extra]}"
483
+ )
484
+ base = [
485
+ x for x in ex_resolve.output_concepts if x.address not in extra
486
+ ]
487
+ for x in root_targets:
488
+ if x.address not in base:
489
+ base.append(x)
490
+ expanded.set_output_concepts(base)
484
491
  # but hide them
485
492
  if pseudonyms:
493
+ expanded.add_output_concepts(pseudonyms)
486
494
  logger.info(
487
495
  f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Hiding pseudonyms{[c.address for c in pseudonyms]}"
488
496
  )
@@ -906,10 +914,10 @@ def _search_concepts(
906
914
  input_concepts=non_virtual,
907
915
  output_concepts=non_virtual,
908
916
  environment=environment,
909
- g=g,
910
917
  parents=stack,
911
918
  depth=depth,
912
919
  )
920
+
913
921
  # ensure we can resolve our final merge
914
922
  output.resolve()
915
923
  if condition_required and conditions:
@@ -919,7 +927,7 @@ def _search_concepts(
919
927
  output, environment, g, where=conditions, history=history
920
928
  )
921
929
  logger.info(
922
- f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Graph is connected, returning merge node, partial {[c.address for c in output.partial_concepts]}"
930
+ f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Graph is connected, returning {type(output)} node partial {[c.address for c in output.partial_concepts]}"
923
931
  )
924
932
  return output
925
933
 
@@ -987,7 +995,6 @@ def source_query_concepts(
987
995
  x for x in root.output_concepts if x.address not in root.hidden_concepts
988
996
  ],
989
997
  environment=environment,
990
- g=g,
991
998
  parents=[root],
992
999
  partial_concepts=root.partial_concepts,
993
1000
  )
@@ -44,7 +44,7 @@ def gen_basic_node(
44
44
  conditions: WhereClause | None = None,
45
45
  ):
46
46
  depth_prefix = "\t" * depth
47
- parent_concepts = resolve_function_parent_concepts(concept)
47
+ parent_concepts = resolve_function_parent_concepts(concept, environment=environment)
48
48
 
49
49
  logger.info(
50
50
  f"{depth_prefix}{LOGGER_PREFIX} basic node for {concept} has parents {[x.address for x in parent_concepts]}"
@@ -61,12 +61,16 @@ def gen_basic_node(
61
61
  f"{depth_prefix}{LOGGER_PREFIX} basic node for {concept} has equivalent optional {[x.address for x in equivalent_optional]}"
62
62
  )
63
63
  for eo in equivalent_optional:
64
- parent_concepts += resolve_function_parent_concepts(eo)
64
+ parent_concepts += resolve_function_parent_concepts(eo, environment=environment)
65
65
  non_equivalent_optional = [
66
66
  x for x in local_optional if x not in equivalent_optional
67
67
  ]
68
+ all_parents = parent_concepts + non_equivalent_optional
69
+ logger.info(
70
+ f"{depth_prefix}{LOGGER_PREFIX} Fetching parents {[x.address for x in all_parents]}"
71
+ )
68
72
  parent_node: StrategyNode = source_concepts(
69
- mandatory_list=parent_concepts + non_equivalent_optional,
73
+ mandatory_list=all_parents,
70
74
  environment=environment,
71
75
  g=g,
72
76
  depth=depth + 1,
@@ -20,12 +20,16 @@ from trilogy.core.processing.nodes.merge_node import MergeNode
20
20
  from trilogy.utility import unique
21
21
 
22
22
 
23
- def resolve_function_parent_concepts(concept: Concept) -> List[Concept]:
23
+ def resolve_function_parent_concepts(
24
+ concept: Concept, environment: Environment
25
+ ) -> List[Concept]:
24
26
  if not isinstance(concept.lineage, (Function, AggregateWrapper)):
25
27
  raise ValueError(f"Concept {concept} lineage is not function or aggregate")
26
28
  if concept.derivation == PurposeLineage.AGGREGATE:
27
29
  if not concept.grain.abstract:
28
- base = concept.lineage.concept_arguments + concept.grain.components_copy
30
+ base = concept.lineage.concept_arguments + [
31
+ environment.concepts[c] for c in concept.grain.components
32
+ ]
29
33
  # if the base concept being aggregated is a property with a key
30
34
  # keep the key as a parent
31
35
  else:
@@ -56,6 +60,7 @@ def resolve_condition_parent_concepts(
56
60
 
57
61
  def resolve_filter_parent_concepts(
58
62
  concept: Concept,
63
+ environment: Environment,
59
64
  ) -> Tuple[Concept, List[Concept], List[Tuple[Concept, ...]]]:
60
65
  if not isinstance(concept.lineage, FilterItem):
61
66
  raise ValueError(
@@ -70,7 +75,7 @@ def resolve_filter_parent_concepts(
70
75
  base_rows += condition_rows
71
76
  base_existence += condition_existence
72
77
  if direct_parent.grain:
73
- base_rows += direct_parent.grain.components_copy
78
+ base_rows += [environment.concepts[c] for c in direct_parent.grain.components]
74
79
  if (
75
80
  isinstance(direct_parent, Concept)
76
81
  and direct_parent.purpose == Purpose.PROPERTY
@@ -130,7 +135,6 @@ def gen_property_enrichment_node(
130
135
  ),
131
136
  output_concepts=base_node.output_concepts + extra_properties,
132
137
  environment=environment,
133
- g=g,
134
138
  parents=[
135
139
  base_node,
136
140
  ]
@@ -209,7 +213,6 @@ def gen_enrichment_node(
209
213
  input_concepts=unique(join_keys + extra_required + non_hidden, "address"),
210
214
  output_concepts=unique(join_keys + extra_required + non_hidden, "address"),
211
215
  environment=environment,
212
- g=g,
213
216
  parents=[enrich_node, base_node],
214
217
  force_group=False,
215
218
  preexisting_conditions=conditions.conditional if conditions else None,
@@ -28,7 +28,7 @@ def gen_filter_node(
28
28
  conditions: WhereClause | None = None,
29
29
  ) -> StrategyNode | None:
30
30
  immediate_parent, parent_row_concepts, parent_existence_concepts = (
31
- resolve_filter_parent_concepts(concept)
31
+ resolve_filter_parent_concepts(concept, environment)
32
32
  )
33
33
  if not isinstance(concept.lineage, FilterItem):
34
34
  raise SyntaxError('Filter node must have a lineage of type "FilterItem"')
@@ -117,7 +117,6 @@ def gen_filter_node(
117
117
  input_concepts=row_parent.output_concepts,
118
118
  output_concepts=[concept] + row_parent.output_concepts,
119
119
  environment=row_parent.environment,
120
- g=row_parent.g,
121
120
  parents=[row_parent],
122
121
  depth=row_parent.depth,
123
122
  partial_concepts=row_parent.partial_concepts,
@@ -137,8 +136,8 @@ def gen_filter_node(
137
136
  parent.add_existence_concepts(flattened_existence, False).set_output_concepts(
138
137
  expected_output, False
139
138
  )
140
- parent.grain = Grain(
141
- components=(
139
+ parent.grain = Grain.from_concepts(
140
+ (
142
141
  list(immediate_parent.keys)
143
142
  if immediate_parent.keys
144
143
  else [immediate_parent]
@@ -161,10 +160,9 @@ def gen_filter_node(
161
160
  ),
162
161
  output_concepts=[concept, immediate_parent] + parent_row_concepts,
163
162
  environment=environment,
164
- g=g,
165
163
  parents=core_parents,
166
- grain=Grain(
167
- components=[immediate_parent] + parent_row_concepts,
164
+ grain=Grain.from_concepts(
165
+ [immediate_parent] + parent_row_concepts,
168
166
  ),
169
167
  preexisting_conditions=conditions.conditional if conditions else None,
170
168
  )
@@ -202,7 +200,6 @@ def gen_filter_node(
202
200
  ]
203
201
  + local_optional,
204
202
  environment=environment,
205
- g=g,
206
203
  parents=[
207
204
  # this node fetches only what we need to filter
208
205
  filter_node,
@@ -34,7 +34,7 @@ def gen_group_node(
34
34
  # aggregates MUST always group to the proper grain
35
35
  # except when the
36
36
  parent_concepts: List[Concept] = unique(
37
- resolve_function_parent_concepts(concept), "address"
37
+ resolve_function_parent_concepts(concept, environment=environment), "address"
38
38
  )
39
39
  logger.info(
40
40
  f"{padding(depth)}{LOGGER_PREFIX} parent concepts are {[x.address for x in parent_concepts]} from group grain {concept.grain}"
@@ -43,18 +43,28 @@ def gen_group_node(
43
43
  # if the aggregation has a grain, we need to ensure these are the ONLY optional in the output of the select
44
44
  output_concepts = [concept]
45
45
 
46
- if concept.grain and len(concept.grain.components_copy) > 0:
47
- grain_components = (
48
- concept.grain.components_copy if not concept.grain.abstract else []
49
- )
46
+ if (
47
+ concept.grain
48
+ and len(concept.grain.components) > 0
49
+ and not concept.grain.abstract
50
+ ):
51
+ grain_components = [environment.concepts[c] for c in concept.grain.components]
50
52
  parent_concepts += grain_components
51
53
  output_concepts += grain_components
52
54
  for possible_agg in local_optional:
55
+
53
56
  if not isinstance(possible_agg.lineage, (AggregateWrapper, Function)):
54
57
  continue
58
+ logger.info(possible_agg)
59
+ if possible_agg.grain and possible_agg.grain != concept.grain:
60
+ logger.info(
61
+ f"{padding(depth)}{LOGGER_PREFIX} mismatched equivalent group by with grain {possible_agg.grain} for {concept.address}"
62
+ )
63
+
55
64
  if possible_agg.grain and possible_agg.grain == concept.grain:
56
65
  agg_parents: List[Concept] = resolve_function_parent_concepts(
57
- possible_agg
66
+ possible_agg,
67
+ environment=environment,
58
68
  )
59
69
  if set([x.address for x in agg_parents]).issubset(
60
70
  set([x.address for x in parent_concepts])
@@ -63,13 +73,19 @@ def gen_group_node(
63
73
  logger.info(
64
74
  f"{padding(depth)}{LOGGER_PREFIX} found equivalent group by optional concept {possible_agg.address} for {concept.address}"
65
75
  )
66
- elif Grain(components=agg_parents) == Grain(components=parent_concepts):
76
+ elif Grain.from_concepts(agg_parents) == Grain.from_concepts(
77
+ parent_concepts
78
+ ):
67
79
  extra = [x for x in agg_parents if x.address not in parent_concepts]
68
80
  parent_concepts += extra
69
81
  output_concepts.append(possible_agg)
70
82
  logger.info(
71
83
  f"{padding(depth)}{LOGGER_PREFIX} found equivalent group by optional concept {possible_agg.address} for {concept.address}"
72
84
  )
85
+ else:
86
+ logger.info(
87
+ f"{padding(depth)}{LOGGER_PREFIX} mismatched grain {Grain.from_concepts(agg_parents)} vs {Grain.from_concepts(parent_concepts)}"
88
+ )
73
89
  if parent_concepts:
74
90
  logger.info(
75
91
  f"{padding(depth)}{LOGGER_PREFIX} fetching group node parents {LooseConceptList(concepts=parent_concepts)}"
@@ -94,13 +110,12 @@ def gen_group_node(
94
110
 
95
111
  # the keys we group by
96
112
  # are what we can use for enrichment
97
- group_key_parents = concept.grain.components_copy
113
+ group_key_parents = [environment.concepts[c] for c in concept.grain.components]
98
114
 
99
115
  group_node = GroupNode(
100
116
  output_concepts=output_concepts,
101
117
  input_concepts=parent_concepts,
102
118
  environment=environment,
103
- g=g,
104
119
  parents=parents,
105
120
  depth=depth,
106
121
  preexisting_conditions=conditions.conditional if conditions else None,
@@ -45,7 +45,6 @@ def gen_group_to_node(
45
45
  output_concepts=parent_concepts + [concept],
46
46
  input_concepts=parent_concepts,
47
47
  environment=environment,
48
- g=g,
49
48
  parents=parents,
50
49
  depth=depth,
51
50
  )
@@ -76,7 +75,6 @@ def gen_group_to_node(
76
75
  + [x for x in parent_concepts if x.address != concept.address],
77
76
  output_concepts=[concept] + local_optional,
78
77
  environment=environment,
79
- g=g,
80
78
  parents=[
81
79
  # this node gets the group
82
80
  group_node,
@@ -8,12 +8,13 @@ from trilogy.core.models import (
8
8
  Concept,
9
9
  Conditional,
10
10
  Environment,
11
+ Grain,
11
12
  MultiSelectStatement,
12
13
  WhereClause,
13
14
  )
14
15
  from trilogy.core.processing.node_generators.common import resolve_join_order
15
16
  from trilogy.core.processing.nodes import History, MergeNode, NodeJoin
16
- from trilogy.core.processing.nodes.base_node import StrategyNode, concept_list_to_grain
17
+ from trilogy.core.processing.nodes.base_node import StrategyNode
17
18
  from trilogy.core.processing.utility import concept_to_relevant_joins, padding
18
19
 
19
20
  LOGGER_PREFIX = "[GEN_MULTISELECT_NODE]"
@@ -108,7 +109,6 @@ def gen_multiselect_node(
108
109
  input_concepts=[x for y in base_parents for x in y.output_concepts],
109
110
  output_concepts=[x for y in base_parents for x in y.output_concepts],
110
111
  environment=environment,
111
- g=g,
112
112
  depth=depth,
113
113
  parents=base_parents,
114
114
  node_joins=node_joins,
@@ -138,8 +138,8 @@ def gen_multiselect_node(
138
138
 
139
139
  # assume grain to be output of select
140
140
  # but don't include anything aggregate at this point
141
- node.resolution_cache.grain = concept_list_to_grain(
142
- node.output_concepts, parent_sources=node.resolution_cache.datasources
141
+ node.resolution_cache.grain = Grain.from_concepts(
142
+ node.output_concepts,
143
143
  )
144
144
  possible_joins = concept_to_relevant_joins(additional_relevant)
145
145
  if not local_optional:
@@ -178,7 +178,6 @@ def gen_multiselect_node(
178
178
  input_concepts=enrich_node.output_concepts + node.output_concepts,
179
179
  output_concepts=node.output_concepts + local_optional,
180
180
  environment=environment,
181
- g=g,
182
181
  depth=depth,
183
182
  parents=[
184
183
  # this node gets the multiselect
@@ -327,13 +327,18 @@ def subgraphs_to_merge_node(
327
327
  for y in x.output_concepts:
328
328
  input_c.append(y)
329
329
  if len(parents) == 1 and enable_early_exit:
330
+ logger.info(
331
+ f"{padding(depth)}{LOGGER_PREFIX} only one parent node, exiting early w/ {[c.address for c in parents[0].output_concepts]}"
332
+ )
330
333
  return parents[0]
331
-
334
+ base_output = [x for x in all_concepts]
335
+ # for x in base_output:
336
+ # if x not in input_c:
337
+ # input_c.append(x)
332
338
  return MergeNode(
333
339
  input_concepts=unique(input_c, "address"),
334
- output_concepts=[x for x in all_concepts],
340
+ output_concepts=base_output,
335
341
  environment=environment,
336
- g=g,
337
342
  parents=parents,
338
343
  depth=depth,
339
344
  # conditions=conditions,
@@ -369,6 +374,12 @@ def gen_merge_node(
369
374
  logger.info(
370
375
  f"{padding(depth)}{LOGGER_PREFIX} Was able to resolve graph through weak component resolution - final graph {log_graph}"
371
376
  )
377
+ for flat in log_graph:
378
+ if set(flat) == set([x.address for x in all_concepts]):
379
+ logger.info(
380
+ f"{padding(depth)}{LOGGER_PREFIX} expanded concept resolution was identical to search resolution; breaking to avoid recursion error."
381
+ )
382
+ return None
372
383
  return subgraphs_to_merge_node(
373
384
  weak_resolve,
374
385
  depth=depth,
@@ -5,6 +5,7 @@ from trilogy.core.enums import PurposeLineage
5
5
  from trilogy.core.models import (
6
6
  Concept,
7
7
  Environment,
8
+ Grain,
8
9
  MultiSelectStatement,
9
10
  RowsetDerivationStatement,
10
11
  RowsetItem,
@@ -12,7 +13,6 @@ from trilogy.core.models import (
12
13
  WhereClause,
13
14
  )
14
15
  from trilogy.core.processing.nodes import History, MergeNode, StrategyNode
15
- from trilogy.core.processing.nodes.base_node import concept_list_to_grain
16
16
  from trilogy.core.processing.utility import concept_to_relevant_joins, padding
17
17
 
18
18
  LOGGER_PREFIX = "[GEN_ROWSET_NODE]"
@@ -74,7 +74,7 @@ def gen_rowset_node(
74
74
  assert node.resolution_cache
75
75
  # assume grain to be output of select
76
76
  # but don't include anything hidden(the non-rowset concepts)
77
- node.grain = concept_list_to_grain(
77
+ node.grain = Grain.from_concepts(
78
78
  [
79
79
  x
80
80
  for x in node.output_concepts
@@ -83,7 +83,6 @@ def gen_rowset_node(
83
83
  y for y in node.hidden_concepts if y.derivation != PurposeLineage.ROWSET
84
84
  ]
85
85
  ],
86
- parent_sources=node.resolution_cache.datasources,
87
86
  )
88
87
 
89
88
  node.rebuild_cache()
@@ -92,7 +91,7 @@ def gen_rowset_node(
92
91
  x.address in node.output_concepts for x in local_optional
93
92
  ):
94
93
  logger.info(
95
- f"{padding(depth)}{LOGGER_PREFIX} no enrichment required for rowset node as all optional found or no optional; exiting early."
94
+ f"{padding(depth)}{LOGGER_PREFIX} no enrichment required for rowset node as all optional {[x.address for x in local_optional]} found or no optional; exiting early."
96
95
  )
97
96
  return node
98
97
  possible_joins = concept_to_relevant_joins(
@@ -140,7 +139,6 @@ def gen_rowset_node(
140
139
  input_concepts=non_hidden + non_hidden_enrich,
141
140
  output_concepts=non_hidden + local_optional,
142
141
  environment=environment,
143
- g=g,
144
142
  depth=depth,
145
143
  parents=[
146
144
  node,
@@ -0,0 +1,203 @@
1
+ from collections import defaultdict
2
+ from datetime import date, datetime, timedelta
3
+ from typing import List, Tuple, TypeVar
4
+
5
+ from trilogy.core.enums import ComparisonOperator
6
+ from trilogy.core.models import (
7
+ Comparison,
8
+ Concept,
9
+ Conditional,
10
+ Datasource,
11
+ DataType,
12
+ Function,
13
+ FunctionType,
14
+ Parenthetical,
15
+ )
16
+
17
+ # Define a generic type that ensures start and end are the same type
18
+ T = TypeVar("T", int, date, datetime)
19
+
20
+
21
+ def reduce_expression(
22
+ var: Concept, group_tuple: list[tuple[ComparisonOperator, T]]
23
+ ) -> bool:
24
+ # Track ranges
25
+ lower_check: T
26
+ upper_check: T
27
+
28
+ # if var.datatype in (DataType.FLOAT,):
29
+ # lower_check = float("-inf") # type: ignore
30
+ # upper_check = float("inf") # type: ignore
31
+ if var.datatype == DataType.INTEGER:
32
+ lower_check = float("-inf") # type: ignore
33
+ upper_check = float("inf") # type: ignore
34
+ elif var.datatype == DataType.DATE:
35
+ lower_check = date.min # type: ignore
36
+ upper_check = date.max # type: ignore
37
+
38
+ elif var.datatype == DataType.DATETIME:
39
+ lower_check = datetime.min # type: ignore
40
+ upper_check = datetime.max # type: ignore
41
+ else:
42
+ raise ValueError(f"Invalid datatype: {var.datatype}")
43
+
44
+ ranges: list[Tuple[T, T]] = []
45
+ for op, value in group_tuple:
46
+ increment: int | timedelta
47
+ if isinstance(value, date):
48
+ increment = timedelta(days=1)
49
+ elif isinstance(value, datetime):
50
+ increment = timedelta(seconds=1)
51
+ elif isinstance(value, int):
52
+ increment = 1
53
+ # elif isinstance(value, float):
54
+ # value = Decimal(value)
55
+ # increment = Decimal(0.0000000001)
56
+
57
+ if op == ">":
58
+ ranges.append(
59
+ (
60
+ value + increment,
61
+ upper_check,
62
+ )
63
+ )
64
+ elif op == ">=":
65
+ ranges.append(
66
+ (
67
+ value,
68
+ upper_check,
69
+ )
70
+ )
71
+ elif op == "<":
72
+ ranges.append(
73
+ (
74
+ lower_check,
75
+ value - increment,
76
+ )
77
+ )
78
+ elif op == "<=":
79
+ ranges.append(
80
+ (
81
+ lower_check,
82
+ value,
83
+ )
84
+ )
85
+ elif op == "=":
86
+ ranges.append(
87
+ (
88
+ value,
89
+ value,
90
+ )
91
+ )
92
+ else:
93
+ raise ValueError(f"Invalid operator: {op}")
94
+ return is_fully_covered(lower_check, upper_check, ranges, increment)
95
+
96
+
97
+ def simplify_conditions(
98
+ conditions: list[Comparison | Conditional | Parenthetical],
99
+ ) -> bool:
100
+ # Group conditions by variable
101
+ grouped: dict[Concept, list[tuple[ComparisonOperator, datetime | int | date]]] = (
102
+ defaultdict(list)
103
+ )
104
+ for condition in conditions:
105
+ if not isinstance(condition, Comparison):
106
+ return False
107
+ if not isinstance(
108
+ condition.left, (int, date, datetime, Function)
109
+ ) and not isinstance(condition.right, (int, date, datetime, Function)):
110
+ return False
111
+ if not isinstance(condition.left, Concept) and not isinstance(
112
+ condition.right, Concept
113
+ ):
114
+ return False
115
+ vars = [condition.left, condition.right]
116
+ concept = [x for x in vars if isinstance(x, Concept)][0]
117
+ comparison = [x for x in vars if not isinstance(x, Concept)][0]
118
+ if isinstance(comparison, Function):
119
+ if not comparison.operator == FunctionType.CONSTANT:
120
+ return False
121
+ first_arg = comparison.arguments[0]
122
+ if not isinstance(first_arg, (int, date, datetime)):
123
+ return False
124
+ comparison = first_arg
125
+ if not isinstance(comparison, (int, date, datetime)):
126
+ return False
127
+
128
+ var = concept
129
+ op = condition.operator
130
+ grouped[var].append((op, comparison))
131
+
132
+ simplified = []
133
+ for var, group_tuple in grouped.items():
134
+ simplified.append(reduce_expression(var, group_tuple)) # type: ignore
135
+
136
+ # Final simplification
137
+ return True if all(isinstance(s, bool) and s for s in simplified) else False
138
+
139
+
140
+ def is_fully_covered(
141
+ start: T,
142
+ end: T,
143
+ ranges: List[Tuple[T, T]],
144
+ increment: int | timedelta,
145
+ ):
146
+ """
147
+ Check if the list of range pairs fully covers the set [start, end].
148
+
149
+ Parameters:
150
+ - start (int or float): The starting value of the set to cover.
151
+ - end (int or float): The ending value of the set to cover.
152
+ - ranges (list of tuples): List of range pairs [(start1, end1), (start2, end2), ...].
153
+
154
+ Returns:
155
+ - bool: True if the ranges fully cover [start, end], False otherwise.
156
+ """
157
+ # Sort ranges by their start values (and by end values for ties)
158
+ ranges.sort()
159
+
160
+ # Check for gaps
161
+ current_end = start
162
+ print(ranges)
163
+ for r_start, r_end in ranges:
164
+ print(r_start, r_end)
165
+ # If there's a gap between the current range and the previous coverage
166
+ print(r_start - current_end)
167
+ if (r_start - current_end) > increment: # type: ignore
168
+ print("gap")
169
+ return False
170
+ print("okay")
171
+ # Extend the current coverage
172
+ current_end = max(current_end, r_end)
173
+
174
+ # If the loop ends and we haven't reached the end, return False
175
+ print(current_end, end)
176
+ print(current_end >= end)
177
+ return current_end >= end
178
+
179
+
180
+ def get_union_sources(datasources: list[Datasource], concepts: list[Concept]):
181
+ candidates: list[Datasource] = []
182
+ for x in datasources:
183
+ if all([c.address in x.output_concepts for c in concepts]):
184
+ if (
185
+ any([c.address in x.partial_concepts for c in concepts])
186
+ and x.non_partial_for
187
+ ):
188
+ candidates.append(x)
189
+
190
+ assocs: dict[str, list[Datasource]] = defaultdict(list[Datasource])
191
+ for x in candidates:
192
+ if not x.non_partial_for:
193
+ continue
194
+ if not len(x.non_partial_for.concept_arguments) == 1:
195
+ continue
196
+ merge_key = x.non_partial_for.concept_arguments[0]
197
+ assocs[merge_key.address].append(x)
198
+ final: list[list[Datasource]] = []
199
+ for _, dses in assocs.items():
200
+ conditions = [c.non_partial_for.conditional for c in dses if c.non_partial_for]
201
+ if simplify_conditions(conditions):
202
+ final.append(dses)
203
+ return final