pytrilogy 0.3.142__cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.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.
- LICENSE.md +19 -0
- _preql_import_resolver/__init__.py +5 -0
- _preql_import_resolver/_preql_import_resolver.cpython-313-x86_64-linux-gnu.so +0 -0
- pytrilogy-0.3.142.dist-info/METADATA +555 -0
- pytrilogy-0.3.142.dist-info/RECORD +200 -0
- pytrilogy-0.3.142.dist-info/WHEEL +5 -0
- pytrilogy-0.3.142.dist-info/entry_points.txt +2 -0
- pytrilogy-0.3.142.dist-info/licenses/LICENSE.md +19 -0
- trilogy/__init__.py +16 -0
- trilogy/ai/README.md +10 -0
- trilogy/ai/__init__.py +19 -0
- trilogy/ai/constants.py +92 -0
- trilogy/ai/conversation.py +107 -0
- trilogy/ai/enums.py +7 -0
- trilogy/ai/execute.py +50 -0
- trilogy/ai/models.py +34 -0
- trilogy/ai/prompts.py +100 -0
- trilogy/ai/providers/__init__.py +0 -0
- trilogy/ai/providers/anthropic.py +106 -0
- trilogy/ai/providers/base.py +24 -0
- trilogy/ai/providers/google.py +146 -0
- trilogy/ai/providers/openai.py +89 -0
- trilogy/ai/providers/utils.py +68 -0
- trilogy/authoring/README.md +3 -0
- trilogy/authoring/__init__.py +148 -0
- trilogy/constants.py +113 -0
- trilogy/core/README.md +52 -0
- trilogy/core/__init__.py +0 -0
- trilogy/core/constants.py +6 -0
- trilogy/core/enums.py +443 -0
- trilogy/core/env_processor.py +120 -0
- trilogy/core/environment_helpers.py +320 -0
- trilogy/core/ergonomics.py +193 -0
- trilogy/core/exceptions.py +123 -0
- trilogy/core/functions.py +1227 -0
- trilogy/core/graph_models.py +139 -0
- trilogy/core/internal.py +85 -0
- trilogy/core/models/__init__.py +0 -0
- trilogy/core/models/author.py +2669 -0
- trilogy/core/models/build.py +2521 -0
- trilogy/core/models/build_environment.py +180 -0
- trilogy/core/models/core.py +501 -0
- trilogy/core/models/datasource.py +322 -0
- trilogy/core/models/environment.py +751 -0
- trilogy/core/models/execute.py +1177 -0
- trilogy/core/optimization.py +251 -0
- trilogy/core/optimizations/__init__.py +12 -0
- trilogy/core/optimizations/base_optimization.py +17 -0
- trilogy/core/optimizations/hide_unused_concept.py +47 -0
- trilogy/core/optimizations/inline_datasource.py +102 -0
- trilogy/core/optimizations/predicate_pushdown.py +245 -0
- trilogy/core/processing/README.md +94 -0
- trilogy/core/processing/READMEv2.md +121 -0
- trilogy/core/processing/VIRTUAL_UNNEST.md +30 -0
- trilogy/core/processing/__init__.py +0 -0
- trilogy/core/processing/concept_strategies_v3.py +508 -0
- trilogy/core/processing/constants.py +15 -0
- trilogy/core/processing/discovery_node_factory.py +451 -0
- trilogy/core/processing/discovery_utility.py +548 -0
- trilogy/core/processing/discovery_validation.py +167 -0
- trilogy/core/processing/graph_utils.py +43 -0
- trilogy/core/processing/node_generators/README.md +9 -0
- trilogy/core/processing/node_generators/__init__.py +31 -0
- trilogy/core/processing/node_generators/basic_node.py +160 -0
- trilogy/core/processing/node_generators/common.py +268 -0
- trilogy/core/processing/node_generators/constant_node.py +38 -0
- trilogy/core/processing/node_generators/filter_node.py +315 -0
- trilogy/core/processing/node_generators/group_node.py +213 -0
- trilogy/core/processing/node_generators/group_to_node.py +117 -0
- trilogy/core/processing/node_generators/multiselect_node.py +205 -0
- trilogy/core/processing/node_generators/node_merge_node.py +653 -0
- trilogy/core/processing/node_generators/recursive_node.py +88 -0
- trilogy/core/processing/node_generators/rowset_node.py +165 -0
- trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
- trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +261 -0
- trilogy/core/processing/node_generators/select_merge_node.py +748 -0
- trilogy/core/processing/node_generators/select_node.py +95 -0
- trilogy/core/processing/node_generators/synonym_node.py +98 -0
- trilogy/core/processing/node_generators/union_node.py +91 -0
- trilogy/core/processing/node_generators/unnest_node.py +182 -0
- trilogy/core/processing/node_generators/window_node.py +201 -0
- trilogy/core/processing/nodes/README.md +28 -0
- trilogy/core/processing/nodes/__init__.py +179 -0
- trilogy/core/processing/nodes/base_node.py +519 -0
- trilogy/core/processing/nodes/filter_node.py +75 -0
- trilogy/core/processing/nodes/group_node.py +194 -0
- trilogy/core/processing/nodes/merge_node.py +420 -0
- trilogy/core/processing/nodes/recursive_node.py +46 -0
- trilogy/core/processing/nodes/select_node_v2.py +242 -0
- trilogy/core/processing/nodes/union_node.py +53 -0
- trilogy/core/processing/nodes/unnest_node.py +62 -0
- trilogy/core/processing/nodes/window_node.py +56 -0
- trilogy/core/processing/utility.py +823 -0
- trilogy/core/query_processor.py +596 -0
- trilogy/core/statements/README.md +35 -0
- trilogy/core/statements/__init__.py +0 -0
- trilogy/core/statements/author.py +536 -0
- trilogy/core/statements/build.py +0 -0
- trilogy/core/statements/common.py +20 -0
- trilogy/core/statements/execute.py +155 -0
- trilogy/core/table_processor.py +66 -0
- trilogy/core/utility.py +8 -0
- trilogy/core/validation/README.md +46 -0
- trilogy/core/validation/__init__.py +0 -0
- trilogy/core/validation/common.py +161 -0
- trilogy/core/validation/concept.py +146 -0
- trilogy/core/validation/datasource.py +227 -0
- trilogy/core/validation/environment.py +73 -0
- trilogy/core/validation/fix.py +256 -0
- trilogy/dialect/__init__.py +32 -0
- trilogy/dialect/base.py +1392 -0
- trilogy/dialect/bigquery.py +308 -0
- trilogy/dialect/common.py +147 -0
- trilogy/dialect/config.py +144 -0
- trilogy/dialect/dataframe.py +50 -0
- trilogy/dialect/duckdb.py +231 -0
- trilogy/dialect/enums.py +147 -0
- trilogy/dialect/metadata.py +173 -0
- trilogy/dialect/mock.py +190 -0
- trilogy/dialect/postgres.py +117 -0
- trilogy/dialect/presto.py +110 -0
- trilogy/dialect/results.py +89 -0
- trilogy/dialect/snowflake.py +129 -0
- trilogy/dialect/sql_server.py +137 -0
- trilogy/engine.py +48 -0
- trilogy/execution/config.py +75 -0
- trilogy/executor.py +568 -0
- trilogy/hooks/__init__.py +4 -0
- trilogy/hooks/base_hook.py +40 -0
- trilogy/hooks/graph_hook.py +139 -0
- trilogy/hooks/query_debugger.py +166 -0
- trilogy/metadata/__init__.py +0 -0
- trilogy/parser.py +10 -0
- trilogy/parsing/README.md +21 -0
- trilogy/parsing/__init__.py +0 -0
- trilogy/parsing/common.py +1069 -0
- trilogy/parsing/config.py +5 -0
- trilogy/parsing/exceptions.py +8 -0
- trilogy/parsing/helpers.py +1 -0
- trilogy/parsing/parse_engine.py +2813 -0
- trilogy/parsing/render.py +769 -0
- trilogy/parsing/trilogy.lark +540 -0
- trilogy/py.typed +0 -0
- trilogy/render.py +42 -0
- trilogy/scripts/README.md +9 -0
- trilogy/scripts/__init__.py +0 -0
- trilogy/scripts/agent.py +41 -0
- trilogy/scripts/agent_info.py +303 -0
- trilogy/scripts/common.py +355 -0
- trilogy/scripts/dependency/Cargo.lock +617 -0
- trilogy/scripts/dependency/Cargo.toml +39 -0
- trilogy/scripts/dependency/README.md +131 -0
- trilogy/scripts/dependency/build.sh +25 -0
- trilogy/scripts/dependency/src/directory_resolver.rs +177 -0
- trilogy/scripts/dependency/src/lib.rs +16 -0
- trilogy/scripts/dependency/src/main.rs +770 -0
- trilogy/scripts/dependency/src/parser.rs +435 -0
- trilogy/scripts/dependency/src/preql.pest +208 -0
- trilogy/scripts/dependency/src/python_bindings.rs +303 -0
- trilogy/scripts/dependency/src/resolver.rs +716 -0
- trilogy/scripts/dependency/tests/base.preql +3 -0
- trilogy/scripts/dependency/tests/cli_integration.rs +377 -0
- trilogy/scripts/dependency/tests/customer.preql +6 -0
- trilogy/scripts/dependency/tests/main.preql +9 -0
- trilogy/scripts/dependency/tests/orders.preql +7 -0
- trilogy/scripts/dependency/tests/test_data/base.preql +9 -0
- trilogy/scripts/dependency/tests/test_data/consumer.preql +1 -0
- trilogy/scripts/dependency.py +323 -0
- trilogy/scripts/display.py +512 -0
- trilogy/scripts/environment.py +46 -0
- trilogy/scripts/fmt.py +32 -0
- trilogy/scripts/ingest.py +471 -0
- trilogy/scripts/ingest_helpers/__init__.py +1 -0
- trilogy/scripts/ingest_helpers/foreign_keys.py +123 -0
- trilogy/scripts/ingest_helpers/formatting.py +93 -0
- trilogy/scripts/ingest_helpers/typing.py +161 -0
- trilogy/scripts/init.py +105 -0
- trilogy/scripts/parallel_execution.py +713 -0
- trilogy/scripts/plan.py +189 -0
- trilogy/scripts/run.py +63 -0
- trilogy/scripts/serve.py +140 -0
- trilogy/scripts/serve_helpers/__init__.py +41 -0
- trilogy/scripts/serve_helpers/file_discovery.py +142 -0
- trilogy/scripts/serve_helpers/index_generation.py +206 -0
- trilogy/scripts/serve_helpers/models.py +38 -0
- trilogy/scripts/single_execution.py +131 -0
- trilogy/scripts/testing.py +119 -0
- trilogy/scripts/trilogy.py +68 -0
- trilogy/std/__init__.py +0 -0
- trilogy/std/color.preql +3 -0
- trilogy/std/date.preql +13 -0
- trilogy/std/display.preql +18 -0
- trilogy/std/geography.preql +22 -0
- trilogy/std/metric.preql +15 -0
- trilogy/std/money.preql +67 -0
- trilogy/std/net.preql +14 -0
- trilogy/std/ranking.preql +7 -0
- trilogy/std/report.preql +5 -0
- trilogy/std/semantic.preql +6 -0
- 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
|
+
)
|
|
File without changes
|
|
@@ -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
|