pytrilogy 0.0.1.102__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 (77) hide show
  1. pytrilogy-0.0.1.102.dist-info/LICENSE.md +19 -0
  2. pytrilogy-0.0.1.102.dist-info/METADATA +277 -0
  3. pytrilogy-0.0.1.102.dist-info/RECORD +77 -0
  4. pytrilogy-0.0.1.102.dist-info/WHEEL +5 -0
  5. pytrilogy-0.0.1.102.dist-info/entry_points.txt +2 -0
  6. pytrilogy-0.0.1.102.dist-info/top_level.txt +1 -0
  7. trilogy/__init__.py +8 -0
  8. trilogy/compiler.py +0 -0
  9. trilogy/constants.py +30 -0
  10. trilogy/core/__init__.py +0 -0
  11. trilogy/core/constants.py +3 -0
  12. trilogy/core/enums.py +270 -0
  13. trilogy/core/env_processor.py +33 -0
  14. trilogy/core/environment_helpers.py +156 -0
  15. trilogy/core/ergonomics.py +187 -0
  16. trilogy/core/exceptions.py +23 -0
  17. trilogy/core/functions.py +320 -0
  18. trilogy/core/graph_models.py +55 -0
  19. trilogy/core/internal.py +37 -0
  20. trilogy/core/models.py +3145 -0
  21. trilogy/core/processing/__init__.py +0 -0
  22. trilogy/core/processing/concept_strategies_v3.py +603 -0
  23. trilogy/core/processing/graph_utils.py +44 -0
  24. trilogy/core/processing/node_generators/__init__.py +25 -0
  25. trilogy/core/processing/node_generators/basic_node.py +71 -0
  26. trilogy/core/processing/node_generators/common.py +239 -0
  27. trilogy/core/processing/node_generators/concept_merge.py +152 -0
  28. trilogy/core/processing/node_generators/filter_node.py +83 -0
  29. trilogy/core/processing/node_generators/group_node.py +92 -0
  30. trilogy/core/processing/node_generators/group_to_node.py +99 -0
  31. trilogy/core/processing/node_generators/merge_node.py +148 -0
  32. trilogy/core/processing/node_generators/multiselect_node.py +189 -0
  33. trilogy/core/processing/node_generators/rowset_node.py +130 -0
  34. trilogy/core/processing/node_generators/select_node.py +328 -0
  35. trilogy/core/processing/node_generators/unnest_node.py +37 -0
  36. trilogy/core/processing/node_generators/window_node.py +85 -0
  37. trilogy/core/processing/nodes/__init__.py +76 -0
  38. trilogy/core/processing/nodes/base_node.py +251 -0
  39. trilogy/core/processing/nodes/filter_node.py +49 -0
  40. trilogy/core/processing/nodes/group_node.py +110 -0
  41. trilogy/core/processing/nodes/merge_node.py +326 -0
  42. trilogy/core/processing/nodes/select_node_v2.py +198 -0
  43. trilogy/core/processing/nodes/unnest_node.py +54 -0
  44. trilogy/core/processing/nodes/window_node.py +34 -0
  45. trilogy/core/processing/utility.py +278 -0
  46. trilogy/core/query_processor.py +331 -0
  47. trilogy/dialect/__init__.py +0 -0
  48. trilogy/dialect/base.py +679 -0
  49. trilogy/dialect/bigquery.py +80 -0
  50. trilogy/dialect/common.py +43 -0
  51. trilogy/dialect/config.py +55 -0
  52. trilogy/dialect/duckdb.py +83 -0
  53. trilogy/dialect/enums.py +95 -0
  54. trilogy/dialect/postgres.py +86 -0
  55. trilogy/dialect/presto.py +82 -0
  56. trilogy/dialect/snowflake.py +82 -0
  57. trilogy/dialect/sql_server.py +89 -0
  58. trilogy/docs/__init__.py +0 -0
  59. trilogy/engine.py +48 -0
  60. trilogy/executor.py +242 -0
  61. trilogy/hooks/__init__.py +0 -0
  62. trilogy/hooks/base_hook.py +37 -0
  63. trilogy/hooks/graph_hook.py +24 -0
  64. trilogy/hooks/query_debugger.py +133 -0
  65. trilogy/metadata/__init__.py +0 -0
  66. trilogy/parser.py +10 -0
  67. trilogy/parsing/__init__.py +0 -0
  68. trilogy/parsing/common.py +176 -0
  69. trilogy/parsing/config.py +5 -0
  70. trilogy/parsing/exceptions.py +2 -0
  71. trilogy/parsing/helpers.py +1 -0
  72. trilogy/parsing/parse_engine.py +1951 -0
  73. trilogy/parsing/render.py +483 -0
  74. trilogy/py.typed +0 -0
  75. trilogy/scripts/__init__.py +0 -0
  76. trilogy/scripts/trilogy.py +127 -0
  77. trilogy/utility.py +31 -0
@@ -0,0 +1,71 @@
1
+ # directly select out a basic derivation
2
+ from typing import List
3
+
4
+ from trilogy.core.models import (
5
+ Concept,
6
+ )
7
+ from trilogy.core.processing.nodes import StrategyNode, History, MergeNode
8
+ from trilogy.core.processing.node_generators.common import (
9
+ resolve_function_parent_concepts,
10
+ )
11
+ from trilogy.constants import logger
12
+
13
+ LOGGER_PREFIX = "[GEN_BASIC_NODE]"
14
+
15
+
16
+ def gen_basic_node(
17
+ concept: Concept,
18
+ local_optional: List[Concept],
19
+ environment,
20
+ g,
21
+ depth: int,
22
+ source_concepts,
23
+ history: History | None = None,
24
+ ):
25
+ depth_prefix = "\t" * depth
26
+ parent_concepts = resolve_function_parent_concepts(concept)
27
+
28
+ logger.info(
29
+ f"{depth_prefix}{LOGGER_PREFIX} basic node for {concept} has parents {[x.address for x in parent_concepts]}"
30
+ )
31
+
32
+ output_concepts = [concept] + local_optional
33
+ partials = []
34
+
35
+ attempts = [(parent_concepts, [concept])]
36
+ if local_optional:
37
+ attempts.append((parent_concepts + local_optional, local_optional + [concept]))
38
+
39
+ for attempt, output in reversed(attempts):
40
+ parent_node = source_concepts(
41
+ mandatory_list=attempt,
42
+ environment=environment,
43
+ g=g,
44
+ depth=depth + 1,
45
+ history=history,
46
+ )
47
+ if not parent_node:
48
+ continue
49
+ parents: List[StrategyNode] = [parent_node]
50
+ for x in output_concepts:
51
+ sources = [p for p in parents if x in p.output_concepts]
52
+ if not sources:
53
+ continue
54
+ if all(x in source.partial_concepts for source in sources):
55
+ partials.append(x)
56
+ logger.info(
57
+ f"{depth_prefix}{LOGGER_PREFIX} Returning basic select for {concept} with attempted extra {[x.address for x in attempt]}"
58
+ )
59
+ return MergeNode(
60
+ input_concepts=attempt,
61
+ output_concepts=output,
62
+ environment=environment,
63
+ g=g,
64
+ parents=parents,
65
+ depth=depth,
66
+ partial_concepts=partials,
67
+ )
68
+ logger.info(
69
+ f"{depth_prefix}{LOGGER_PREFIX} No basic node could be generated for {concept}"
70
+ )
71
+ return None
@@ -0,0 +1,239 @@
1
+ from typing import List, Tuple
2
+
3
+
4
+ from trilogy.core.enums import PurposeLineage, Purpose
5
+ from trilogy.core.models import (
6
+ Concept,
7
+ Function,
8
+ AggregateWrapper,
9
+ FilterItem,
10
+ Environment,
11
+ LooseConceptList,
12
+ )
13
+ from trilogy.utility import unique
14
+ from trilogy.core.processing.nodes.base_node import StrategyNode
15
+ from trilogy.core.processing.nodes.merge_node import MergeNode
16
+ from trilogy.core.processing.nodes import History
17
+ from trilogy.core.enums import JoinType
18
+ from trilogy.core.processing.nodes import (
19
+ NodeJoin,
20
+ )
21
+ from collections import defaultdict
22
+ from trilogy.core.processing.utility import concept_to_relevant_joins
23
+
24
+
25
+ def resolve_function_parent_concepts(concept: Concept) -> List[Concept]:
26
+ if not isinstance(concept.lineage, (Function, AggregateWrapper)):
27
+ raise ValueError(f"Concept {concept} lineage is not function or aggregate")
28
+ if concept.derivation == PurposeLineage.AGGREGATE:
29
+ if not concept.grain.abstract:
30
+ base = concept.lineage.concept_arguments + concept.grain.components_copy
31
+ # if the base concept being aggregated is a property with a key
32
+ # keep the key as a parent
33
+ else:
34
+ base = concept.lineage.concept_arguments
35
+ if isinstance(concept.lineage, AggregateWrapper):
36
+ # for aggregate wrapper, don't include the by
37
+ extra_property_grain = concept.lineage.function.concept_arguments
38
+ else:
39
+ extra_property_grain = concept.lineage.concept_arguments
40
+ for x in extra_property_grain:
41
+ if isinstance(x, Concept) and x.purpose == Purpose.PROPERTY and x.keys:
42
+ base += x.keys
43
+ return unique(base, "address")
44
+ # TODO: handle basic lineage chains?
45
+ return unique(concept.lineage.concept_arguments, "address")
46
+
47
+
48
+ def resolve_filter_parent_concepts(concept: Concept) -> Tuple[Concept, List[Concept]]:
49
+ if not isinstance(concept.lineage, FilterItem):
50
+ raise ValueError
51
+ direct_parent = concept.lineage.content
52
+ base = [direct_parent]
53
+ base += concept.lineage.where.concept_arguments
54
+ if direct_parent.grain:
55
+ base += direct_parent.grain.components_copy
56
+ if (
57
+ isinstance(direct_parent, Concept)
58
+ and direct_parent.purpose == Purpose.PROPERTY
59
+ and direct_parent.keys
60
+ ):
61
+ base += direct_parent.keys
62
+ return concept.lineage.content, unique(base, "address")
63
+
64
+
65
+ def gen_property_enrichment_node(
66
+ base_node: StrategyNode,
67
+ extra_properties: list[Concept],
68
+ environment: Environment,
69
+ g,
70
+ depth: int,
71
+ source_concepts,
72
+ history: History | None = None,
73
+ ):
74
+ required_keys: dict[str, set[str]] = defaultdict(set)
75
+ for x in extra_properties:
76
+ if not x.keys:
77
+ raise SyntaxError(f"Property {x.address} missing keys in lookup")
78
+ keys = "-".join([y.address for y in x.keys])
79
+ required_keys[keys].add(x.address)
80
+ final_nodes = []
81
+ node_joins = []
82
+ for _k, vs in required_keys.items():
83
+ ks = _k.split("-")
84
+ enrich_node: StrategyNode = source_concepts(
85
+ mandatory_list=[environment.concepts[k] for k in ks]
86
+ + [environment.concepts[v] for v in vs],
87
+ environment=environment,
88
+ g=g,
89
+ depth=depth + 1,
90
+ history=history,
91
+ )
92
+ final_nodes.append(enrich_node)
93
+ node_joins.append(
94
+ NodeJoin(
95
+ left_node=enrich_node,
96
+ right_node=base_node,
97
+ concepts=concept_to_relevant_joins(
98
+ [environment.concepts[k] for k in ks]
99
+ ),
100
+ filter_to_mutual=False,
101
+ join_type=JoinType.LEFT_OUTER,
102
+ )
103
+ )
104
+ return MergeNode(
105
+ input_concepts=unique(
106
+ base_node.output_concepts
107
+ + extra_properties
108
+ + [
109
+ environment.concepts[v]
110
+ for k, values in required_keys.items()
111
+ for v in values
112
+ ],
113
+ "address",
114
+ ),
115
+ output_concepts=base_node.output_concepts + extra_properties,
116
+ environment=environment,
117
+ g=g,
118
+ parents=[
119
+ base_node,
120
+ enrich_node,
121
+ ],
122
+ node_joins=node_joins,
123
+ )
124
+
125
+
126
+ def gen_enrichment_node(
127
+ base_node: StrategyNode,
128
+ join_keys: List[Concept],
129
+ local_optional: list[Concept],
130
+ environment: Environment,
131
+ g,
132
+ depth: int,
133
+ source_concepts,
134
+ log_lambda,
135
+ history: History | None = None,
136
+ ):
137
+
138
+ local_opts = LooseConceptList(concepts=local_optional)
139
+
140
+ if local_opts.issubset(LooseConceptList(concepts=base_node.output_concepts)):
141
+ log_lambda(
142
+ f"{str(type(base_node).__name__)} has all optional { base_node.output_lcl}, skipping enrichmennt"
143
+ )
144
+ return base_node
145
+ extra_required = [
146
+ x
147
+ for x in local_opts
148
+ if x not in base_node.output_lcl or x in base_node.partial_lcl
149
+ ]
150
+
151
+ # property lookup optimization
152
+ # this helps when evaluating a normalized star schema as you only want to lookup the missing properties based on the relevant keys
153
+ if all([x.purpose == Purpose.PROPERTY for x in extra_required]):
154
+ if all(
155
+ x.keys and all([key in base_node.output_lcl for key in x.keys])
156
+ for x in extra_required
157
+ ):
158
+ log_lambda(
159
+ f"{str(type(base_node).__name__)} returning property optimized enrichment node"
160
+ )
161
+ return gen_property_enrichment_node(
162
+ base_node,
163
+ extra_required,
164
+ environment,
165
+ g,
166
+ depth,
167
+ source_concepts,
168
+ history=history,
169
+ )
170
+
171
+ enrich_node: StrategyNode = source_concepts( # this fetches the parent + join keys
172
+ # to then connect to the rest of the query
173
+ mandatory_list=join_keys + extra_required,
174
+ environment=environment,
175
+ g=g,
176
+ depth=depth,
177
+ history=history,
178
+ )
179
+ if not enrich_node:
180
+ log_lambda(
181
+ f"{str(type(base_node).__name__)} enrichment node unresolvable, returning just group node"
182
+ )
183
+ return base_node
184
+ log_lambda(
185
+ f"{str(type(base_node).__name__)} returning merge node with group node + enrichment node"
186
+ )
187
+ return MergeNode(
188
+ input_concepts=unique(
189
+ join_keys + extra_required + base_node.output_concepts, "address"
190
+ ),
191
+ output_concepts=unique(
192
+ join_keys + extra_required + base_node.output_concepts, "address"
193
+ ),
194
+ environment=environment,
195
+ g=g,
196
+ parents=[enrich_node, base_node],
197
+ node_joins=[
198
+ NodeJoin(
199
+ left_node=enrich_node,
200
+ right_node=base_node,
201
+ concepts=concept_to_relevant_joins(
202
+ [x for x in join_keys if x in enrich_node.output_lcl]
203
+ ),
204
+ filter_to_mutual=False,
205
+ join_type=JoinType.LEFT_OUTER,
206
+ )
207
+ ],
208
+ )
209
+
210
+
211
+ def resolve_join_order(joins: List[NodeJoin]) -> List[NodeJoin]:
212
+ available_aliases: set[StrategyNode] = set()
213
+ final_joins_pre = [*joins]
214
+ final_joins = []
215
+ while final_joins_pre:
216
+ new_final_joins_pre: List[NodeJoin] = []
217
+ for join in final_joins_pre:
218
+ if not available_aliases:
219
+ final_joins.append(join)
220
+ available_aliases.add(join.left_node)
221
+ available_aliases.add(join.right_node)
222
+ elif join.left_node in available_aliases:
223
+ # we don't need to join twice
224
+ # so whatever join we found first, works
225
+ if join.right_node in available_aliases:
226
+ continue
227
+ final_joins.append(join)
228
+ available_aliases.add(join.left_node)
229
+ available_aliases.add(join.right_node)
230
+ else:
231
+ new_final_joins_pre.append(join)
232
+ if len(new_final_joins_pre) == len(final_joins_pre):
233
+ remaining = [join.left_node for join in new_final_joins_pre]
234
+ remaining_right = [join.right_node for join in new_final_joins_pre]
235
+ raise SyntaxError(
236
+ f"did not find any new joins, available {available_aliases} remaining is {remaining + remaining_right} "
237
+ )
238
+ final_joins_pre = new_final_joins_pre
239
+ return final_joins
@@ -0,0 +1,152 @@
1
+ from trilogy.core.models import (
2
+ Concept,
3
+ Environment,
4
+ MergeStatement,
5
+ )
6
+ from trilogy.core.processing.nodes import MergeNode, NodeJoin, History
7
+ from trilogy.core.processing.nodes.base_node import concept_list_to_grain, StrategyNode
8
+ from typing import List
9
+
10
+ from trilogy.core.enums import JoinType
11
+ from trilogy.constants import logger
12
+ from trilogy.core.processing.utility import padding
13
+ from trilogy.core.processing.node_generators.common import concept_to_relevant_joins
14
+ from itertools import combinations
15
+ from trilogy.core.processing.node_generators.common import resolve_join_order
16
+
17
+ LOGGER_PREFIX = "[GEN_CONCEPT_MERGE_NODE]"
18
+
19
+
20
+ def merge_joins(base: MergeStatement, parents: List[StrategyNode]) -> List[NodeJoin]:
21
+ output = []
22
+ for left, right in combinations(parents, 2):
23
+ output.append(
24
+ NodeJoin(
25
+ left_node=left,
26
+ right_node=right,
27
+ concepts=[
28
+ base.merge_concept,
29
+ ],
30
+ join_type=JoinType.FULL,
31
+ )
32
+ )
33
+ return resolve_join_order(output)
34
+
35
+
36
+ def gen_concept_merge_node(
37
+ concept: Concept,
38
+ local_optional: List[Concept],
39
+ environment: Environment,
40
+ g,
41
+ depth: int,
42
+ source_concepts,
43
+ history: History | None = None,
44
+ ) -> MergeNode | None:
45
+ if not isinstance(concept.lineage, MergeStatement):
46
+ logger.info(
47
+ f"{padding(depth)}{LOGGER_PREFIX} Cannot generate merge node for {concept}"
48
+ )
49
+ return None
50
+ lineage: MergeStatement = concept.lineage
51
+
52
+ base_parents: List[StrategyNode] = []
53
+ for select in lineage.concepts:
54
+ # if it's a merge concept, filter it out of the optional
55
+ sub_optional = [
56
+ x
57
+ for x in local_optional
58
+ if x.address not in lineage.concepts_lcl and x.namespace == select.namespace
59
+ ]
60
+ snode: StrategyNode = source_concepts(
61
+ mandatory_list=[select] + sub_optional,
62
+ environment=environment,
63
+ g=g,
64
+ depth=depth + 1,
65
+ history=history,
66
+ )
67
+ if not snode:
68
+ logger.info(
69
+ f"{padding(depth)}{LOGGER_PREFIX} Cannot generate merge node for {concept}"
70
+ )
71
+ return None
72
+ snode.add_output_concept(lineage.merge_concept)
73
+ base_parents.append(snode)
74
+
75
+ node_joins = merge_joins(lineage, base_parents)
76
+
77
+ enrichment = set([x.address for x in local_optional])
78
+ outputs = [x for y in base_parents for x in y.output_concepts]
79
+
80
+ additional_relevant = [x for x in outputs if x.address in enrichment]
81
+ node = MergeNode(
82
+ input_concepts=[x for y in base_parents for x in y.output_concepts],
83
+ output_concepts=outputs + additional_relevant + [concept],
84
+ environment=environment,
85
+ g=g,
86
+ depth=depth,
87
+ parents=base_parents,
88
+ node_joins=node_joins,
89
+ )
90
+
91
+ qds = node.rebuild_cache()
92
+
93
+ # assume grain to be outoput of select
94
+ # but don't include anything aggregate at this point
95
+ qds.grain = concept_list_to_grain(
96
+ node.output_concepts, parent_sources=qds.datasources
97
+ )
98
+ possible_joins = concept_to_relevant_joins(additional_relevant)
99
+ if not local_optional:
100
+ logger.info(
101
+ f"{padding(depth)}{LOGGER_PREFIX} no enriched required for merge concept node; exiting early"
102
+ )
103
+ return node
104
+ if not possible_joins:
105
+ logger.info(
106
+ f"{padding(depth)}{LOGGER_PREFIX} no possible joins for merge concept node; exiting early"
107
+ )
108
+ return node
109
+ if all(
110
+ [x.address in [y.address for y in node.output_concepts] for x in local_optional]
111
+ ):
112
+ logger.info(
113
+ f"{padding(depth)}{LOGGER_PREFIX} all enriched concepts returned from base merge concept node; exiting early"
114
+ )
115
+ return node
116
+ enrich_node: MergeNode = source_concepts( # this fetches the parent + join keys
117
+ # to then connect to the rest of the query
118
+ mandatory_list=additional_relevant + local_optional,
119
+ environment=environment,
120
+ g=g,
121
+ depth=depth + 1,
122
+ history=history,
123
+ )
124
+ if not enrich_node:
125
+ logger.info(
126
+ f"{padding(depth)}{LOGGER_PREFIX} Cannot generate merge concept enrichment node for {concept} with optional {local_optional}, returning just merge concept"
127
+ )
128
+ return node
129
+
130
+ return MergeNode(
131
+ input_concepts=enrich_node.output_concepts + node.output_concepts,
132
+ output_concepts=node.output_concepts + local_optional,
133
+ environment=environment,
134
+ g=g,
135
+ depth=depth,
136
+ parents=[
137
+ # this node gets the window
138
+ node,
139
+ # this node gets enrichment
140
+ enrich_node,
141
+ ],
142
+ node_joins=[
143
+ NodeJoin(
144
+ left_node=enrich_node,
145
+ right_node=node,
146
+ concepts=possible_joins,
147
+ filter_to_mutual=False,
148
+ join_type=JoinType.LEFT_OUTER,
149
+ )
150
+ ],
151
+ partial_concepts=node.partial_concepts,
152
+ )
@@ -0,0 +1,83 @@
1
+ from typing import List
2
+
3
+
4
+ from trilogy.core.enums import JoinType
5
+ from trilogy.core.models import (
6
+ Concept,
7
+ Environment,
8
+ )
9
+ from trilogy.core.processing.nodes import FilterNode, MergeNode, NodeJoin, History
10
+ from trilogy.core.processing.node_generators.common import (
11
+ resolve_filter_parent_concepts,
12
+ )
13
+ from trilogy.constants import logger
14
+ from trilogy.core.processing.utility import padding
15
+ from trilogy.core.processing.node_generators.common import concept_to_relevant_joins
16
+
17
+ LOGGER_PREFIX = "[GEN_FILTER_NODE]"
18
+
19
+
20
+ def gen_filter_node(
21
+ concept: Concept,
22
+ local_optional: List[Concept],
23
+ environment: Environment,
24
+ g,
25
+ depth: int,
26
+ source_concepts,
27
+ history: History | None = None,
28
+ ) -> MergeNode | FilterNode | None:
29
+ immediate_parent, parent_concepts = resolve_filter_parent_concepts(concept)
30
+
31
+ logger.info(f"{padding(depth)}{LOGGER_PREFIX} fetching filter node parents")
32
+ parent = source_concepts(
33
+ mandatory_list=parent_concepts,
34
+ environment=environment,
35
+ g=g,
36
+ depth=depth + 1,
37
+ history=history,
38
+ )
39
+ if not parent:
40
+ return None
41
+ filter_node = FilterNode(
42
+ input_concepts=[immediate_parent] + parent_concepts,
43
+ output_concepts=[concept, immediate_parent] + parent_concepts,
44
+ environment=environment,
45
+ g=g,
46
+ parents=[parent],
47
+ )
48
+ if not local_optional:
49
+ return filter_node
50
+ enrich_node = source_concepts( # this fetches the parent + join keys
51
+ # to then connect to the rest of the query
52
+ mandatory_list=[immediate_parent] + parent_concepts + local_optional,
53
+ environment=environment,
54
+ g=g,
55
+ depth=depth + 1,
56
+ history=history,
57
+ )
58
+ x = MergeNode(
59
+ input_concepts=[concept, immediate_parent] + local_optional,
60
+ output_concepts=[
61
+ concept,
62
+ ]
63
+ + local_optional,
64
+ environment=environment,
65
+ g=g,
66
+ parents=[
67
+ # this node fetches only what we need to filter
68
+ filter_node,
69
+ enrich_node,
70
+ ],
71
+ node_joins=[
72
+ NodeJoin(
73
+ left_node=enrich_node,
74
+ right_node=filter_node,
75
+ concepts=concept_to_relevant_joins(
76
+ [immediate_parent] + parent_concepts
77
+ ),
78
+ join_type=JoinType.LEFT_OUTER,
79
+ filter_to_mutual=False,
80
+ )
81
+ ],
82
+ )
83
+ return x
@@ -0,0 +1,92 @@
1
+ from trilogy.core.models import Concept, Environment, LooseConceptList
2
+ from trilogy.utility import unique
3
+ from trilogy.core.processing.nodes import GroupNode, StrategyNode, History
4
+ from typing import List
5
+ from trilogy.core.processing.node_generators.common import (
6
+ resolve_function_parent_concepts,
7
+ )
8
+ from trilogy.constants import logger
9
+ from trilogy.core.processing.utility import padding, create_log_lambda
10
+ from trilogy.core.processing.node_generators.common import (
11
+ gen_enrichment_node,
12
+ )
13
+
14
+ LOGGER_PREFIX = "[GEN_GROUP_NODE]"
15
+
16
+
17
+ def gen_group_node(
18
+ concept: Concept,
19
+ local_optional: List[Concept],
20
+ environment: Environment,
21
+ g,
22
+ depth: int,
23
+ source_concepts,
24
+ history: History | None = None,
25
+ ):
26
+ # aggregates MUST always group to the proper grain
27
+ # except when the
28
+ parent_concepts: List[Concept] = unique(
29
+ resolve_function_parent_concepts(concept), "address"
30
+ )
31
+ logger.info(
32
+ f"{padding(depth)}{LOGGER_PREFIX} parent_concepts are {[x.address for x in parent_concepts]} from group grain {concept.grain}"
33
+ )
34
+
35
+ # if the aggregation has a grain, we need to ensure these are the ONLY optional in the output of the select
36
+ output_concepts = [concept]
37
+
38
+ if concept.grain and len(concept.grain.components_copy) > 0:
39
+ grain_components = (
40
+ concept.grain.components_copy if not concept.grain.abstract else []
41
+ )
42
+ parent_concepts += grain_components
43
+ output_concepts += grain_components
44
+
45
+ if parent_concepts:
46
+ logger.info(
47
+ f"{padding(depth)}{LOGGER_PREFIX} fetching group node parents {LooseConceptList(concepts=parent_concepts)}"
48
+ )
49
+ parent_concepts = unique(parent_concepts, "address")
50
+ parent = source_concepts(
51
+ mandatory_list=parent_concepts,
52
+ environment=environment,
53
+ g=g,
54
+ depth=depth,
55
+ history=history,
56
+ )
57
+ if not parent:
58
+ logger.info(
59
+ f"{padding(depth)}{LOGGER_PREFIX} group by node parents unresolvable"
60
+ )
61
+ return None
62
+ parents: List[StrategyNode] = [parent]
63
+ else:
64
+ parents = []
65
+
66
+ # the keys we group by
67
+ # are what we can use for enrichment
68
+ group_key_parents = concept.grain.components_copy
69
+
70
+ group_node = GroupNode(
71
+ output_concepts=output_concepts,
72
+ input_concepts=parent_concepts,
73
+ environment=environment,
74
+ g=g,
75
+ parents=parents,
76
+ depth=depth,
77
+ )
78
+
79
+ # early exit if no optional
80
+ if not local_optional:
81
+ return group_node
82
+ logger.info(f"{padding(depth)}{LOGGER_PREFIX} group node requires enrichment")
83
+ return gen_enrichment_node(
84
+ group_node,
85
+ join_keys=group_key_parents,
86
+ local_optional=local_optional,
87
+ environment=environment,
88
+ g=g,
89
+ depth=depth,
90
+ source_concepts=source_concepts,
91
+ log_lambda=create_log_lambda(LOGGER_PREFIX, depth, logger),
92
+ )