pytrilogy 0.3.142__cp312-cp312-win_amd64.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 (200) hide show
  1. LICENSE.md +19 -0
  2. _preql_import_resolver/__init__.py +5 -0
  3. _preql_import_resolver/_preql_import_resolver.cp312-win_amd64.pyd +0 -0
  4. pytrilogy-0.3.142.dist-info/METADATA +555 -0
  5. pytrilogy-0.3.142.dist-info/RECORD +200 -0
  6. pytrilogy-0.3.142.dist-info/WHEEL +4 -0
  7. pytrilogy-0.3.142.dist-info/entry_points.txt +2 -0
  8. pytrilogy-0.3.142.dist-info/licenses/LICENSE.md +19 -0
  9. trilogy/__init__.py +16 -0
  10. trilogy/ai/README.md +10 -0
  11. trilogy/ai/__init__.py +19 -0
  12. trilogy/ai/constants.py +92 -0
  13. trilogy/ai/conversation.py +107 -0
  14. trilogy/ai/enums.py +7 -0
  15. trilogy/ai/execute.py +50 -0
  16. trilogy/ai/models.py +34 -0
  17. trilogy/ai/prompts.py +100 -0
  18. trilogy/ai/providers/__init__.py +0 -0
  19. trilogy/ai/providers/anthropic.py +106 -0
  20. trilogy/ai/providers/base.py +24 -0
  21. trilogy/ai/providers/google.py +146 -0
  22. trilogy/ai/providers/openai.py +89 -0
  23. trilogy/ai/providers/utils.py +68 -0
  24. trilogy/authoring/README.md +3 -0
  25. trilogy/authoring/__init__.py +148 -0
  26. trilogy/constants.py +113 -0
  27. trilogy/core/README.md +52 -0
  28. trilogy/core/__init__.py +0 -0
  29. trilogy/core/constants.py +6 -0
  30. trilogy/core/enums.py +443 -0
  31. trilogy/core/env_processor.py +120 -0
  32. trilogy/core/environment_helpers.py +320 -0
  33. trilogy/core/ergonomics.py +193 -0
  34. trilogy/core/exceptions.py +123 -0
  35. trilogy/core/functions.py +1227 -0
  36. trilogy/core/graph_models.py +139 -0
  37. trilogy/core/internal.py +85 -0
  38. trilogy/core/models/__init__.py +0 -0
  39. trilogy/core/models/author.py +2669 -0
  40. trilogy/core/models/build.py +2521 -0
  41. trilogy/core/models/build_environment.py +180 -0
  42. trilogy/core/models/core.py +501 -0
  43. trilogy/core/models/datasource.py +322 -0
  44. trilogy/core/models/environment.py +751 -0
  45. trilogy/core/models/execute.py +1177 -0
  46. trilogy/core/optimization.py +251 -0
  47. trilogy/core/optimizations/__init__.py +12 -0
  48. trilogy/core/optimizations/base_optimization.py +17 -0
  49. trilogy/core/optimizations/hide_unused_concept.py +47 -0
  50. trilogy/core/optimizations/inline_datasource.py +102 -0
  51. trilogy/core/optimizations/predicate_pushdown.py +245 -0
  52. trilogy/core/processing/README.md +94 -0
  53. trilogy/core/processing/READMEv2.md +121 -0
  54. trilogy/core/processing/VIRTUAL_UNNEST.md +30 -0
  55. trilogy/core/processing/__init__.py +0 -0
  56. trilogy/core/processing/concept_strategies_v3.py +508 -0
  57. trilogy/core/processing/constants.py +15 -0
  58. trilogy/core/processing/discovery_node_factory.py +451 -0
  59. trilogy/core/processing/discovery_utility.py +548 -0
  60. trilogy/core/processing/discovery_validation.py +167 -0
  61. trilogy/core/processing/graph_utils.py +43 -0
  62. trilogy/core/processing/node_generators/README.md +9 -0
  63. trilogy/core/processing/node_generators/__init__.py +31 -0
  64. trilogy/core/processing/node_generators/basic_node.py +160 -0
  65. trilogy/core/processing/node_generators/common.py +268 -0
  66. trilogy/core/processing/node_generators/constant_node.py +38 -0
  67. trilogy/core/processing/node_generators/filter_node.py +315 -0
  68. trilogy/core/processing/node_generators/group_node.py +213 -0
  69. trilogy/core/processing/node_generators/group_to_node.py +117 -0
  70. trilogy/core/processing/node_generators/multiselect_node.py +205 -0
  71. trilogy/core/processing/node_generators/node_merge_node.py +653 -0
  72. trilogy/core/processing/node_generators/recursive_node.py +88 -0
  73. trilogy/core/processing/node_generators/rowset_node.py +165 -0
  74. trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
  75. trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +261 -0
  76. trilogy/core/processing/node_generators/select_merge_node.py +748 -0
  77. trilogy/core/processing/node_generators/select_node.py +95 -0
  78. trilogy/core/processing/node_generators/synonym_node.py +98 -0
  79. trilogy/core/processing/node_generators/union_node.py +91 -0
  80. trilogy/core/processing/node_generators/unnest_node.py +182 -0
  81. trilogy/core/processing/node_generators/window_node.py +201 -0
  82. trilogy/core/processing/nodes/README.md +28 -0
  83. trilogy/core/processing/nodes/__init__.py +179 -0
  84. trilogy/core/processing/nodes/base_node.py +519 -0
  85. trilogy/core/processing/nodes/filter_node.py +75 -0
  86. trilogy/core/processing/nodes/group_node.py +194 -0
  87. trilogy/core/processing/nodes/merge_node.py +420 -0
  88. trilogy/core/processing/nodes/recursive_node.py +46 -0
  89. trilogy/core/processing/nodes/select_node_v2.py +242 -0
  90. trilogy/core/processing/nodes/union_node.py +53 -0
  91. trilogy/core/processing/nodes/unnest_node.py +62 -0
  92. trilogy/core/processing/nodes/window_node.py +56 -0
  93. trilogy/core/processing/utility.py +823 -0
  94. trilogy/core/query_processor.py +596 -0
  95. trilogy/core/statements/README.md +35 -0
  96. trilogy/core/statements/__init__.py +0 -0
  97. trilogy/core/statements/author.py +536 -0
  98. trilogy/core/statements/build.py +0 -0
  99. trilogy/core/statements/common.py +20 -0
  100. trilogy/core/statements/execute.py +155 -0
  101. trilogy/core/table_processor.py +66 -0
  102. trilogy/core/utility.py +8 -0
  103. trilogy/core/validation/README.md +46 -0
  104. trilogy/core/validation/__init__.py +0 -0
  105. trilogy/core/validation/common.py +161 -0
  106. trilogy/core/validation/concept.py +146 -0
  107. trilogy/core/validation/datasource.py +227 -0
  108. trilogy/core/validation/environment.py +73 -0
  109. trilogy/core/validation/fix.py +256 -0
  110. trilogy/dialect/__init__.py +32 -0
  111. trilogy/dialect/base.py +1392 -0
  112. trilogy/dialect/bigquery.py +308 -0
  113. trilogy/dialect/common.py +147 -0
  114. trilogy/dialect/config.py +144 -0
  115. trilogy/dialect/dataframe.py +50 -0
  116. trilogy/dialect/duckdb.py +231 -0
  117. trilogy/dialect/enums.py +147 -0
  118. trilogy/dialect/metadata.py +173 -0
  119. trilogy/dialect/mock.py +190 -0
  120. trilogy/dialect/postgres.py +117 -0
  121. trilogy/dialect/presto.py +110 -0
  122. trilogy/dialect/results.py +89 -0
  123. trilogy/dialect/snowflake.py +129 -0
  124. trilogy/dialect/sql_server.py +137 -0
  125. trilogy/engine.py +48 -0
  126. trilogy/execution/config.py +75 -0
  127. trilogy/executor.py +568 -0
  128. trilogy/hooks/__init__.py +4 -0
  129. trilogy/hooks/base_hook.py +40 -0
  130. trilogy/hooks/graph_hook.py +139 -0
  131. trilogy/hooks/query_debugger.py +166 -0
  132. trilogy/metadata/__init__.py +0 -0
  133. trilogy/parser.py +10 -0
  134. trilogy/parsing/README.md +21 -0
  135. trilogy/parsing/__init__.py +0 -0
  136. trilogy/parsing/common.py +1069 -0
  137. trilogy/parsing/config.py +5 -0
  138. trilogy/parsing/exceptions.py +8 -0
  139. trilogy/parsing/helpers.py +1 -0
  140. trilogy/parsing/parse_engine.py +2813 -0
  141. trilogy/parsing/render.py +769 -0
  142. trilogy/parsing/trilogy.lark +540 -0
  143. trilogy/py.typed +0 -0
  144. trilogy/render.py +42 -0
  145. trilogy/scripts/README.md +9 -0
  146. trilogy/scripts/__init__.py +0 -0
  147. trilogy/scripts/agent.py +41 -0
  148. trilogy/scripts/agent_info.py +303 -0
  149. trilogy/scripts/common.py +355 -0
  150. trilogy/scripts/dependency/Cargo.lock +617 -0
  151. trilogy/scripts/dependency/Cargo.toml +39 -0
  152. trilogy/scripts/dependency/README.md +131 -0
  153. trilogy/scripts/dependency/build.sh +25 -0
  154. trilogy/scripts/dependency/src/directory_resolver.rs +177 -0
  155. trilogy/scripts/dependency/src/lib.rs +16 -0
  156. trilogy/scripts/dependency/src/main.rs +770 -0
  157. trilogy/scripts/dependency/src/parser.rs +435 -0
  158. trilogy/scripts/dependency/src/preql.pest +208 -0
  159. trilogy/scripts/dependency/src/python_bindings.rs +303 -0
  160. trilogy/scripts/dependency/src/resolver.rs +716 -0
  161. trilogy/scripts/dependency/tests/base.preql +3 -0
  162. trilogy/scripts/dependency/tests/cli_integration.rs +377 -0
  163. trilogy/scripts/dependency/tests/customer.preql +6 -0
  164. trilogy/scripts/dependency/tests/main.preql +9 -0
  165. trilogy/scripts/dependency/tests/orders.preql +7 -0
  166. trilogy/scripts/dependency/tests/test_data/base.preql +9 -0
  167. trilogy/scripts/dependency/tests/test_data/consumer.preql +1 -0
  168. trilogy/scripts/dependency.py +323 -0
  169. trilogy/scripts/display.py +512 -0
  170. trilogy/scripts/environment.py +46 -0
  171. trilogy/scripts/fmt.py +32 -0
  172. trilogy/scripts/ingest.py +471 -0
  173. trilogy/scripts/ingest_helpers/__init__.py +1 -0
  174. trilogy/scripts/ingest_helpers/foreign_keys.py +123 -0
  175. trilogy/scripts/ingest_helpers/formatting.py +93 -0
  176. trilogy/scripts/ingest_helpers/typing.py +161 -0
  177. trilogy/scripts/init.py +105 -0
  178. trilogy/scripts/parallel_execution.py +713 -0
  179. trilogy/scripts/plan.py +189 -0
  180. trilogy/scripts/run.py +63 -0
  181. trilogy/scripts/serve.py +140 -0
  182. trilogy/scripts/serve_helpers/__init__.py +41 -0
  183. trilogy/scripts/serve_helpers/file_discovery.py +142 -0
  184. trilogy/scripts/serve_helpers/index_generation.py +206 -0
  185. trilogy/scripts/serve_helpers/models.py +38 -0
  186. trilogy/scripts/single_execution.py +131 -0
  187. trilogy/scripts/testing.py +119 -0
  188. trilogy/scripts/trilogy.py +68 -0
  189. trilogy/std/__init__.py +0 -0
  190. trilogy/std/color.preql +3 -0
  191. trilogy/std/date.preql +13 -0
  192. trilogy/std/display.preql +18 -0
  193. trilogy/std/geography.preql +22 -0
  194. trilogy/std/metric.preql +15 -0
  195. trilogy/std/money.preql +67 -0
  196. trilogy/std/net.preql +14 -0
  197. trilogy/std/ranking.preql +7 -0
  198. trilogy/std/report.preql +5 -0
  199. trilogy/std/semantic.preql +6 -0
  200. trilogy/utility.py +34 -0
@@ -0,0 +1,88 @@
1
+ from typing import List
2
+
3
+ from trilogy.constants import DEFAULT_NAMESPACE, RECURSIVE_GATING_CONCEPT, logger
4
+ from trilogy.core.models.build import (
5
+ BuildComparison,
6
+ BuildConcept,
7
+ BuildFunction,
8
+ BuildGrain,
9
+ BuildWhereClause,
10
+ ComparisonOperator,
11
+ DataType,
12
+ Derivation,
13
+ Purpose,
14
+ )
15
+ from trilogy.core.models.build_environment import BuildEnvironment
16
+ from trilogy.core.processing.nodes import History, RecursiveNode, StrategyNode
17
+ from trilogy.core.processing.utility import padding
18
+
19
+ LOGGER_PREFIX = "[GEN_RECURSIVE_NODE]"
20
+
21
+ GATING_CONCEPT = BuildConcept(
22
+ name=RECURSIVE_GATING_CONCEPT,
23
+ canonical_name=RECURSIVE_GATING_CONCEPT,
24
+ namespace=DEFAULT_NAMESPACE,
25
+ grain=BuildGrain(),
26
+ build_is_aggregate=False,
27
+ datatype=DataType.BOOL,
28
+ purpose=Purpose.KEY,
29
+ derivation=Derivation.BASIC,
30
+ )
31
+
32
+
33
+ def gen_recursive_node(
34
+ concept: BuildConcept,
35
+ local_optional: List[BuildConcept],
36
+ history: History,
37
+ environment: BuildEnvironment,
38
+ g,
39
+ depth: int,
40
+ source_concepts,
41
+ conditions: BuildWhereClause | None = None,
42
+ ) -> StrategyNode | None:
43
+ arguments = []
44
+ if isinstance(concept.lineage, BuildFunction):
45
+ arguments = concept.lineage.concept_arguments
46
+ logger.info(
47
+ f"{padding(depth)}{LOGGER_PREFIX} Fetching recursive node for {concept.name} with arguments {arguments} and conditions {conditions}"
48
+ )
49
+ parent = source_concepts(
50
+ mandatory_list=arguments,
51
+ environment=environment,
52
+ g=g,
53
+ depth=depth + 1,
54
+ history=history,
55
+ # conditions=conditions,
56
+ )
57
+ if not parent:
58
+ logger.info(
59
+ f"{padding(depth)}{LOGGER_PREFIX} could not find recursive node parents"
60
+ )
61
+ return None
62
+ outputs = (
63
+ [concept]
64
+ + arguments
65
+ + [
66
+ GATING_CONCEPT,
67
+ ]
68
+ )
69
+ base = RecursiveNode(
70
+ input_concepts=arguments,
71
+ output_concepts=outputs,
72
+ environment=environment,
73
+ parents=([parent] if (arguments or local_optional) else []),
74
+ # preexisting_conditions=conditions
75
+ )
76
+ # TODO:
77
+ # recursion will result in a union; group up to our final targets
78
+ wrapped_base = StrategyNode(
79
+ input_concepts=outputs,
80
+ output_concepts=outputs,
81
+ environment=environment,
82
+ parents=[base],
83
+ depth=depth,
84
+ conditions=BuildComparison(
85
+ left=GATING_CONCEPT, right=True, operator=ComparisonOperator.IS
86
+ ),
87
+ )
88
+ return wrapped_base
@@ -0,0 +1,165 @@
1
+ from typing import List
2
+
3
+ from trilogy.constants import logger
4
+ from trilogy.core.enums import Derivation
5
+ from trilogy.core.exceptions import UnresolvableQueryException
6
+ from trilogy.core.models.author import MultiSelectLineage, SelectLineage
7
+ from trilogy.core.models.build import (
8
+ BuildConcept,
9
+ BuildGrain,
10
+ BuildRowsetItem,
11
+ BuildRowsetLineage,
12
+ BuildWhereClause,
13
+ Factory,
14
+ )
15
+ from trilogy.core.models.build_environment import BuildEnvironment
16
+ from trilogy.core.processing.nodes import History, MergeNode, StrategyNode
17
+ from trilogy.core.processing.utility import concept_to_relevant_joins, padding
18
+
19
+ LOGGER_PREFIX = "[GEN_ROWSET_NODE]"
20
+
21
+
22
+ def gen_rowset_node(
23
+ concept: BuildConcept,
24
+ local_optional: List[BuildConcept],
25
+ environment: BuildEnvironment,
26
+ g,
27
+ depth: int,
28
+ source_concepts,
29
+ history: History,
30
+ conditions: BuildWhereClause | None = None,
31
+ ) -> StrategyNode | None:
32
+ from trilogy.core.query_processor import get_query_node
33
+
34
+ if not isinstance(concept.lineage, BuildRowsetItem):
35
+ raise SyntaxError(
36
+ f"Invalid lineage passed into rowset fetch, got {type(concept.lineage)}, expected {BuildRowsetItem}"
37
+ )
38
+ lineage: BuildRowsetItem = concept.lineage
39
+ rowset: BuildRowsetLineage = lineage.rowset
40
+ select: SelectLineage | MultiSelectLineage = lineage.rowset.select
41
+
42
+ node = get_query_node(history.base_environment, select)
43
+
44
+ if not node:
45
+ logger.info(
46
+ f"{padding(depth)}{LOGGER_PREFIX} Cannot generate parent rowset node for {concept}"
47
+ )
48
+ raise UnresolvableQueryException(
49
+ f"Cannot generate parent select for concept {concept} in rowset {rowset.name}; ensure the rowset is a valid statement."
50
+ )
51
+ enrichment = set([x.address for x in local_optional])
52
+
53
+ factory = Factory(environment=history.base_environment, grain=select.grain)
54
+ logger.info(
55
+ f"{padding(depth)}{LOGGER_PREFIX} rowset derived concepts are {lineage.rowset.derived_concepts}"
56
+ )
57
+ concept_pool = list(environment.concepts.values()) + list(
58
+ environment.alias_origin_lookup.values()
59
+ )
60
+ rowset_outputs = [
61
+ x.address for x in concept_pool if x.address in lineage.rowset.derived_concepts
62
+ ]
63
+ rowset_relevant: list[BuildConcept] = [
64
+ v for v in concept_pool if v.address in rowset_outputs
65
+ ]
66
+
67
+ additional_relevant = [
68
+ factory.build(x) for x in select.output_components if x.address in enrichment
69
+ ]
70
+ # add in other other concepts
71
+ node.set_output_concepts(rowset_relevant + additional_relevant)
72
+ if select.where_clause:
73
+ for item in additional_relevant:
74
+ logger.info(
75
+ f"{padding(depth)}{LOGGER_PREFIX} adding {item} to partial concepts"
76
+ )
77
+ node.partial_concepts.append(item)
78
+
79
+ node.grain = BuildGrain.from_concepts(
80
+ [
81
+ x
82
+ for x in node.output_concepts
83
+ if x.address
84
+ not in [
85
+ y
86
+ for y in node.hidden_concepts
87
+ if y in environment.concepts
88
+ and environment.concepts[y].derivation != Derivation.ROWSET
89
+ ]
90
+ ],
91
+ )
92
+
93
+ node.rebuild_cache()
94
+ logger.info(
95
+ f"{padding(depth)}{LOGGER_PREFIX} final output is {[x.address for x in node.output_concepts]} with grain {node.grain}"
96
+ )
97
+ if not local_optional or all(
98
+ (
99
+ x.address in node.output_concepts
100
+ or (z in x.pseudonyms for z in node.output_concepts)
101
+ )
102
+ and x.address not in node.partial_concepts
103
+ for x in local_optional
104
+ ):
105
+ logger.info(
106
+ 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."
107
+ )
108
+ return node
109
+ remaining = [
110
+ x
111
+ for x in local_optional
112
+ if x not in node.output_concepts or x in node.partial_concepts
113
+ ]
114
+ possible_joins = concept_to_relevant_joins(
115
+ [x for x in node.output_concepts if x.derivation != Derivation.ROWSET]
116
+ )
117
+ if not possible_joins:
118
+ logger.info(
119
+ f"{padding(depth)}{LOGGER_PREFIX} no possible joins for rowset node to get {[x.address for x in local_optional]}; have {[x.address for x in node.output_concepts]}"
120
+ )
121
+ return node
122
+ if any(x.derivation == Derivation.ROWSET for x in possible_joins):
123
+
124
+ logger.info(
125
+ f"{padding(depth)}{LOGGER_PREFIX} cannot enrich rowset node with rowset concepts; exiting early"
126
+ )
127
+ return node
128
+ logger.info([x.address for x in possible_joins + local_optional])
129
+ enrich_node: MergeNode = source_concepts( # this fetches the parent + join keys
130
+ # to then connect to the rest of the query
131
+ mandatory_list=possible_joins + remaining,
132
+ environment=environment,
133
+ g=g,
134
+ depth=depth + 1,
135
+ conditions=conditions,
136
+ history=history,
137
+ )
138
+ if not enrich_node:
139
+ logger.info(
140
+ f"{padding(depth)}{LOGGER_PREFIX} Cannot generate rowset enrichment node for {concept} with optional {local_optional}, returning just rowset node"
141
+ )
142
+ return node
143
+
144
+ non_hidden = [
145
+ x for x in node.output_concepts if x.address not in node.hidden_concepts
146
+ ]
147
+ for x in possible_joins:
148
+ if x.address in node.hidden_concepts:
149
+ node.unhide_output_concepts([x])
150
+ non_hidden_enrich = [
151
+ x
152
+ for x in enrich_node.output_concepts
153
+ if x.address not in enrich_node.hidden_concepts
154
+ ]
155
+ return MergeNode(
156
+ input_concepts=non_hidden + non_hidden_enrich,
157
+ output_concepts=non_hidden + local_optional,
158
+ environment=environment,
159
+ depth=depth,
160
+ parents=[
161
+ node,
162
+ enrich_node,
163
+ ],
164
+ preexisting_conditions=conditions.conditional if conditions else None,
165
+ )
@@ -0,0 +1,261 @@
1
+ import sys
2
+ from collections import defaultdict
3
+ from datetime import date, datetime, timedelta
4
+ from typing import List, Tuple, TypeVar
5
+
6
+ from trilogy.core.enums import ComparisonOperator, FunctionType
7
+ from trilogy.core.models.build import (
8
+ BuildComparison,
9
+ BuildConcept,
10
+ BuildConditional,
11
+ BuildDatasource,
12
+ BuildFunction,
13
+ BuildParenthetical,
14
+ )
15
+ from trilogy.core.models.core import DataType
16
+
17
+ # Define a generic type that ensures start and end are the same type
18
+ T = TypeVar("T", int, float, date, datetime)
19
+
20
+
21
+ def reduce_expression(
22
+ var: BuildConcept, 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
+ elif var.datatype == DataType.BOOL:
42
+ lower_check = False # type: ignore
43
+ upper_check = True # type: ignore
44
+ elif var.datatype == DataType.FLOAT:
45
+ lower_check = float("-inf") # type: ignore
46
+ upper_check = float("inf") # type: ignore
47
+ else:
48
+ return False
49
+
50
+ ranges: list[Tuple[T, T]] = []
51
+ for op, value in group_tuple:
52
+ increment: int | timedelta | float
53
+ if isinstance(value, date):
54
+ increment = timedelta(days=1)
55
+ elif isinstance(value, datetime):
56
+ increment = timedelta(seconds=1)
57
+ elif isinstance(value, int):
58
+ increment = 1
59
+ elif isinstance(value, float):
60
+ increment = sys.float_info.epsilon
61
+
62
+ if op == ">":
63
+ ranges.append(
64
+ (
65
+ value + increment,
66
+ upper_check,
67
+ )
68
+ )
69
+ elif op == ">=":
70
+ ranges.append(
71
+ (
72
+ value,
73
+ upper_check,
74
+ )
75
+ )
76
+ elif op == "<":
77
+ ranges.append(
78
+ (
79
+ lower_check,
80
+ value - increment,
81
+ )
82
+ )
83
+ elif op == "<=":
84
+ ranges.append(
85
+ (
86
+ lower_check,
87
+ value,
88
+ )
89
+ )
90
+ elif op == "=":
91
+ ranges.append(
92
+ (
93
+ value,
94
+ value,
95
+ )
96
+ )
97
+ elif op == ComparisonOperator.IS:
98
+ ranges.append(
99
+ (
100
+ value,
101
+ value,
102
+ )
103
+ )
104
+ elif op == ComparisonOperator.NE:
105
+ pass
106
+ else:
107
+ return False
108
+ return is_fully_covered(lower_check, upper_check, ranges, increment)
109
+
110
+
111
+ TARGET_TYPES = (
112
+ int,
113
+ date,
114
+ float,
115
+ datetime,
116
+ bool,
117
+ )
118
+ REDUCABLE_TYPES = (int, float, date, bool, datetime, BuildFunction)
119
+
120
+
121
+ def simplify_conditions(
122
+ conditions: list[BuildComparison | BuildConditional | BuildParenthetical],
123
+ ) -> bool:
124
+ # Group conditions by variable
125
+ grouped: dict[
126
+ BuildConcept, list[tuple[ComparisonOperator, datetime | int | date | float]]
127
+ ] = defaultdict(list)
128
+ for condition in conditions:
129
+ if not isinstance(condition, BuildComparison):
130
+ return False
131
+ left_is_concept = False
132
+ left_is_reducable = False
133
+ right_is_concept = False
134
+ right_is_reducable = False
135
+ if isinstance(condition.left, BuildConcept):
136
+ left_is_concept = True
137
+ elif isinstance(condition.left, REDUCABLE_TYPES):
138
+ left_is_reducable = True
139
+
140
+ if isinstance(condition.right, BuildConcept):
141
+ right_is_concept = True
142
+ elif isinstance(condition.right, REDUCABLE_TYPES):
143
+ right_is_reducable = True
144
+
145
+ if not (
146
+ (left_is_concept and right_is_reducable)
147
+ or (right_is_concept and left_is_reducable)
148
+ ):
149
+ return False
150
+ if left_is_concept:
151
+ concept = condition.left
152
+ raw_comparison = condition.right
153
+ else:
154
+ concept = condition.right
155
+ raw_comparison = condition.left
156
+
157
+ if isinstance(raw_comparison, BuildFunction):
158
+ if not raw_comparison.operator == FunctionType.CONSTANT:
159
+ return False
160
+ first_arg = raw_comparison.arguments[0]
161
+ if not isinstance(first_arg, TARGET_TYPES):
162
+ return False
163
+ comparison = first_arg
164
+ else:
165
+ if not isinstance(raw_comparison, TARGET_TYPES):
166
+ return False
167
+ comparison = raw_comparison
168
+
169
+ if not isinstance(comparison, REDUCABLE_TYPES):
170
+ return False
171
+
172
+ var: BuildConcept = concept # type: ignore
173
+ op = condition.operator
174
+ grouped[var].append((op, comparison))
175
+
176
+ simplified = []
177
+ for var, group_tuple in grouped.items():
178
+ simplified.append(reduce_expression(var, group_tuple)) # type: ignore
179
+
180
+ # Final simplification
181
+ return True if all(isinstance(s, bool) and s for s in simplified) else False
182
+
183
+
184
+ def boolean_fully_covered(
185
+ start: bool,
186
+ end: bool,
187
+ ranges: List[Tuple[bool, bool]],
188
+ ):
189
+ all = []
190
+ for r_start, r_end in ranges:
191
+ if r_start is True and r_end is True:
192
+ all.append(True)
193
+ elif r_start is False and r_end is False:
194
+ all.append(False)
195
+ return set(all) == {False, True}
196
+
197
+
198
+ def is_fully_covered(
199
+ start: T,
200
+ end: T,
201
+ ranges: List[Tuple[T, T]],
202
+ increment: int | timedelta | float,
203
+ ):
204
+ """
205
+ Check if the list of range pairs fully covers the set [start, end].
206
+
207
+ Parameters:
208
+ - start (int or float): The starting value of the set to cover.
209
+ - end (int or float): The ending value of the set to cover.
210
+ - ranges (list of tuples): List of range pairs [(start1, end1), (start2, end2), ...].
211
+
212
+ Returns:
213
+ - bool: True if the ranges fully cover [start, end], False otherwise.
214
+ """
215
+ if isinstance(start, bool) and isinstance(end, bool):
216
+ # convert each element of each tuple to a boolean
217
+ bool_ranges = [(bool(r_start), bool(r_end)) for r_start, r_end in ranges]
218
+
219
+ return boolean_fully_covered(start, end, bool_ranges)
220
+ # Sort ranges by their start values (and by end values for ties)
221
+ ranges.sort()
222
+
223
+ # Check for gaps
224
+ current_end = start
225
+ for r_start, r_end in ranges:
226
+ # If there's a gap between the current range and the previous coverage
227
+ if (r_start - current_end) > increment: # type: ignore
228
+ return False
229
+ # Extend the current coverage
230
+ current_end = max(current_end, r_end)
231
+
232
+ # If the loop ends and we haven't reached the end, return False
233
+ return current_end >= end
234
+
235
+
236
+ def get_union_sources(
237
+ datasources: list[BuildDatasource], concepts: list[BuildConcept]
238
+ ) -> List[list[BuildDatasource]]:
239
+ candidates: list[BuildDatasource] = []
240
+
241
+ for x in datasources:
242
+ if any([c.address in x.output_concepts for c in concepts]):
243
+ if (
244
+ any([c.address in x.partial_concepts for c in concepts])
245
+ and x.non_partial_for
246
+ ):
247
+ candidates.append(x)
248
+ assocs: dict[str, list[BuildDatasource]] = defaultdict(list[BuildDatasource])
249
+ for x in candidates:
250
+ if not x.non_partial_for:
251
+ continue
252
+ if not len(x.non_partial_for.concept_arguments) == 1:
253
+ continue
254
+ merge_key = x.non_partial_for.concept_arguments[0]
255
+ assocs[merge_key.address].append(x)
256
+ final: list[list[BuildDatasource]] = []
257
+ for _, dses in assocs.items():
258
+ conditions = [c.non_partial_for.conditional for c in dses if c.non_partial_for]
259
+ if simplify_conditions(conditions):
260
+ final.append(dses)
261
+ return final