pytrilogy 0.0.2.50__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 (27) hide show
  1. {pytrilogy-0.0.2.50.dist-info → pytrilogy-0.0.2.51.dist-info}/METADATA +1 -1
  2. {pytrilogy-0.0.2.50.dist-info → pytrilogy-0.0.2.51.dist-info}/RECORD +27 -25
  3. trilogy/__init__.py +1 -1
  4. trilogy/core/internal.py +5 -1
  5. trilogy/core/models.py +124 -263
  6. trilogy/core/processing/concept_strategies_v3.py +14 -4
  7. trilogy/core/processing/node_generators/basic_node.py +7 -3
  8. trilogy/core/processing/node_generators/common.py +8 -3
  9. trilogy/core/processing/node_generators/filter_node.py +5 -5
  10. trilogy/core/processing/node_generators/group_node.py +24 -8
  11. trilogy/core/processing/node_generators/multiselect_node.py +4 -3
  12. trilogy/core/processing/node_generators/node_merge_node.py +14 -2
  13. trilogy/core/processing/node_generators/rowset_node.py +3 -4
  14. trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
  15. trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +203 -0
  16. trilogy/core/processing/node_generators/select_merge_node.py +17 -9
  17. trilogy/core/processing/nodes/base_node.py +2 -33
  18. trilogy/core/processing/nodes/group_node.py +19 -10
  19. trilogy/core/processing/nodes/merge_node.py +2 -2
  20. trilogy/hooks/graph_hook.py +3 -1
  21. trilogy/parsing/common.py +54 -12
  22. trilogy/parsing/parse_engine.py +39 -20
  23. trilogy/parsing/render.py +8 -1
  24. {pytrilogy-0.0.2.50.dist-info → pytrilogy-0.0.2.51.dist-info}/LICENSE.md +0 -0
  25. {pytrilogy-0.0.2.50.dist-info → pytrilogy-0.0.2.51.dist-info}/WHEEL +0 -0
  26. {pytrilogy-0.0.2.50.dist-info → pytrilogy-0.0.2.51.dist-info}/entry_points.txt +0 -0
  27. {pytrilogy-0.0.2.50.dist-info → pytrilogy-0.0.2.51.dist-info}/top_level.txt +0 -0
@@ -15,10 +15,10 @@ from trilogy.core.models import (
15
15
  )
16
16
  from trilogy.core.processing.nodes.base_node import (
17
17
  StrategyNode,
18
- concept_list_to_grain,
19
18
  resolve_concept_map,
20
19
  )
21
20
  from trilogy.core.processing.utility import find_nullable_concepts, is_scalar_condition
21
+ from trilogy.parsing.common import concepts_to_grain_concepts
22
22
  from trilogy.utility import unique
23
23
 
24
24
  LOGGER_PREFIX = "[CONCEPT DETAIL - GROUP NODE]"
@@ -64,19 +64,27 @@ class GroupNode(StrategyNode):
64
64
  p.resolve() for p in self.parents
65
65
  ]
66
66
 
67
- grain = self.grain or concept_list_to_grain(self.output_concepts, [])
67
+ target_grain = self.grain or Grain.from_concepts(
68
+ concepts_to_grain_concepts(
69
+ self.output_concepts, environment=self.environment
70
+ )
71
+ )
68
72
  comp_grain = Grain()
69
73
  for source in parent_sources:
70
74
  comp_grain += source.grain
71
-
75
+ comp_grain = Grain.from_concepts(
76
+ concepts_to_grain_concepts(
77
+ comp_grain.components, environment=self.environment
78
+ )
79
+ )
72
80
  # dynamically select if we need to group
73
81
  # because sometimes, we are already at required grain
74
- if comp_grain == grain and self.force_group is not True:
82
+ if comp_grain == target_grain and self.force_group is not True:
75
83
  # if there is no group by, and inputs equal outputs
76
84
  # return the parent
77
85
  logger.info(
78
86
  f"{self.logging_prefix}{LOGGER_PREFIX} Grain of group by equals output"
79
- f" grains {comp_grain} and {grain}"
87
+ f" grains {comp_grain} and {target_grain}"
80
88
  )
81
89
  if (
82
90
  len(parent_sources) == 1
@@ -94,10 +102,11 @@ class GroupNode(StrategyNode):
94
102
  source_type = SourceType.SELECT
95
103
  else:
96
104
  logger.info(
97
- f"{self.logging_prefix}{LOGGER_PREFIX} Group node has different grain than parents; forcing group"
98
- f" upstream grains {[str(source.grain) for source in parent_sources]}"
105
+ f"{self.logging_prefix}{LOGGER_PREFIX} Group node has different grain than parents; group is required."
106
+ f" Upstream grains {[str(source.grain) for source in parent_sources]}"
99
107
  f" with final grain {comp_grain} vs"
100
- f" target grain {grain}"
108
+ f" target grain {target_grain}"
109
+ f" delta: {comp_grain - target_grain}"
101
110
  )
102
111
  for parent in self.parents:
103
112
  logger.info(
@@ -134,7 +143,7 @@ class GroupNode(StrategyNode):
134
143
  source_type=source_type,
135
144
  source_map=source_map,
136
145
  joins=[],
137
- grain=grain,
146
+ grain=target_grain,
138
147
  partial_concepts=self.partial_concepts,
139
148
  nullable_concepts=nullable_concepts,
140
149
  hidden_concepts=self.hidden_concepts,
@@ -163,7 +172,7 @@ class GroupNode(StrategyNode):
163
172
  source_type=SourceType.SELECT,
164
173
  source_map=source_map,
165
174
  joins=[],
166
- grain=grain,
175
+ grain=target_grain,
167
176
  nullable_concepts=base.nullable_concepts,
168
177
  partial_concepts=self.partial_concepts,
169
178
  condition=self.conditions,
@@ -58,7 +58,7 @@ def deduplicate_nodes(
58
58
  og = merged[k1]
59
59
  subset_to = merged[k2]
60
60
  logger.info(
61
- f"{logging_prefix}{LOGGER_PREFIX} extraneous parent node that is subset of another parent node {og.grain.issubset(subset_to.grain)} {og.grain.set} {subset_to.grain.set}"
61
+ f"{logging_prefix}{LOGGER_PREFIX} extraneous parent node that is subset of another parent node {og.grain.issubset(subset_to.grain)} {og.grain.components} {subset_to.grain.components}"
62
62
  )
63
63
  merged = {k: v for k, v in merged.items() if k != k1}
64
64
  removed.add(k1)
@@ -197,7 +197,7 @@ class MergeNode(StrategyNode):
197
197
  ) -> List[BaseJoin | UnnestJoin]:
198
198
  # only finally, join between them for unique values
199
199
  dataset_list: List[QueryDatasource | Datasource] = sorted(
200
- final_datasets, key=lambda x: -len(x.grain.components_copy)
200
+ final_datasets, key=lambda x: -len(x.grain.components)
201
201
  )
202
202
 
203
203
  logger.info(
@@ -30,6 +30,7 @@ class GraphHook(BaseHook):
30
30
  graph: nx.DiGraph,
31
31
  target: str | None = None,
32
32
  highlight_nodes: list[str] | None = None,
33
+ remove_isolates: bool = True,
33
34
  ):
34
35
  from matplotlib import pyplot as plt
35
36
 
@@ -38,7 +39,8 @@ class GraphHook(BaseHook):
38
39
  for node in nodes:
39
40
  if "__preql_internal" in node:
40
41
  graph.remove_node(node)
41
- graph.remove_nodes_from(list(nx.isolates(graph)))
42
+ if remove_isolates:
43
+ graph.remove_nodes_from(list(nx.isolates(graph)))
42
44
  color_map = []
43
45
  highlight_nodes = highlight_nodes or []
44
46
  for node in graph:
trilogy/parsing/common.py CHANGED
@@ -4,7 +4,13 @@ from typing import List, Tuple
4
4
  from trilogy.constants import (
5
5
  VIRTUAL_CONCEPT_PREFIX,
6
6
  )
7
- from trilogy.core.enums import FunctionType, Modifier, PurposeLineage, WindowType
7
+ from trilogy.core.enums import (
8
+ FunctionType,
9
+ Granularity,
10
+ Modifier,
11
+ PurposeLineage,
12
+ WindowType,
13
+ )
8
14
  from trilogy.core.functions import arg_to_datatype, function_args_to_output_purpose
9
15
  from trilogy.core.models import (
10
16
  AggregateWrapper,
@@ -66,7 +72,7 @@ def process_function_args(
66
72
  concept = function_to_concept(
67
73
  arg,
68
74
  name=f"{VIRTUAL_CONCEPT_PREFIX}_{arg.operator.value}_{id_hash}",
69
- namespace=environment.namespace,
75
+ environment=environment,
70
76
  )
71
77
  # to satisfy mypy, concept will always have metadata
72
78
  if concept.metadata and meta:
@@ -80,7 +86,7 @@ def process_function_args(
80
86
  concept = arbitrary_to_concept(
81
87
  arg,
82
88
  name=f"{VIRTUAL_CONCEPT_PREFIX}_{id_hash}",
83
- namespace=environment.namespace,
89
+ environment=environment,
84
90
  )
85
91
  if concept.metadata and meta:
86
92
  concept.metadata.line_number = meta.line
@@ -139,10 +145,39 @@ def constant_to_concept(
139
145
  )
140
146
 
141
147
 
148
+ def concepts_to_grain_concepts(
149
+ concepts: List[Concept] | List[str] | set[str], environment: Environment | None
150
+ ) -> list[Concept]:
151
+ environment = Environment() if environment is None else environment
152
+ pconcepts: list[Concept] = [
153
+ c if isinstance(c, Concept) else environment.concepts[c] for c in concepts
154
+ ]
155
+
156
+ final: List[Concept] = []
157
+ for sub in pconcepts:
158
+ if sub.purpose in (Purpose.PROPERTY, Purpose.METRIC) and sub.keys:
159
+ if any([c.address in pconcepts for c in sub.keys]):
160
+ continue
161
+ if sub.purpose in (Purpose.METRIC,):
162
+ if all([c in pconcepts for c in sub.grain.components]):
163
+ continue
164
+ if sub.granularity == Granularity.SINGLE_ROW:
165
+ continue
166
+ final.append(sub)
167
+ final = unique(final, "address")
168
+ v2 = sorted(final, key=lambda x: x.name)
169
+ return v2
170
+
171
+
142
172
  def function_to_concept(
143
- parent: Function, name: str, namespace: str, metadata: Metadata | None = None
173
+ parent: Function,
174
+ name: str,
175
+ environment: Environment,
176
+ namespace: str | None = None,
177
+ metadata: Metadata | None = None,
144
178
  ) -> Concept:
145
179
  pkeys: List[Concept] = []
180
+ namespace = namespace or environment.namespace
146
181
  for x in parent.arguments:
147
182
  pkeys += [
148
183
  x
@@ -162,7 +197,7 @@ def function_to_concept(
162
197
  key_grain += [*x.keys]
163
198
  else:
164
199
  key_grain.append(x)
165
- keys = tuple(Grain(components=key_grain).components_copy)
200
+ keys = tuple(concepts_to_grain_concepts(key_grain, environment))
166
201
  if not pkeys:
167
202
  purpose = Purpose.CONSTANT
168
203
  else:
@@ -256,7 +291,7 @@ def window_item_to_concept(
256
291
  lineage=parent,
257
292
  metadata=fmetadata,
258
293
  # filters are implicitly at the grain of the base item
259
- grain=Grain(components=grain),
294
+ grain=Grain.from_concepts(grain),
260
295
  namespace=namespace,
261
296
  keys=keys,
262
297
  modifiers=modifiers,
@@ -268,9 +303,8 @@ def agg_wrapper_to_concept(
268
303
  namespace: str,
269
304
  name: str,
270
305
  metadata: Metadata | None = None,
271
- purpose: Purpose | None = None,
272
306
  ) -> Concept:
273
- local_purpose, keys = get_purpose_and_keys(
307
+ _, keys = get_purpose_and_keys(
274
308
  Purpose.METRIC, tuple(parent.by) if parent.by else None
275
309
  )
276
310
  # anything grouped to a grain should be a property
@@ -284,7 +318,7 @@ def agg_wrapper_to_concept(
284
318
  purpose=Purpose.METRIC,
285
319
  metadata=fmetadata,
286
320
  lineage=parent,
287
- grain=Grain(components=parent.by) if parent.by else Grain(),
321
+ grain=Grain.from_concepts(parent.by) if parent.by else Grain(),
288
322
  namespace=namespace,
289
323
  keys=tuple(parent.by) if parent.by else keys,
290
324
  modifiers=modifiers,
@@ -304,15 +338,17 @@ def arbitrary_to_concept(
304
338
  | float
305
339
  | str
306
340
  ),
307
- namespace: str,
341
+ environment: Environment,
342
+ namespace: str | None = None,
308
343
  name: str | None = None,
309
344
  metadata: Metadata | None = None,
310
345
  purpose: Purpose | None = None,
311
346
  ) -> Concept:
347
+ namespace = namespace or environment.namespace
312
348
  if isinstance(parent, AggregateWrapper):
313
349
  if not name:
314
350
  name = f"{VIRTUAL_CONCEPT_PREFIX}_agg_{parent.function.operator.value}_{string_to_hash(str(parent))}"
315
- return agg_wrapper_to_concept(parent, namespace, name, metadata, purpose)
351
+ return agg_wrapper_to_concept(parent, namespace, name, metadata)
316
352
  elif isinstance(parent, WindowItem):
317
353
  if not name:
318
354
  name = f"{VIRTUAL_CONCEPT_PREFIX}_window_{parent.type.value}_{string_to_hash(str(parent))}"
@@ -324,7 +360,13 @@ def arbitrary_to_concept(
324
360
  elif isinstance(parent, Function):
325
361
  if not name:
326
362
  name = f"{VIRTUAL_CONCEPT_PREFIX}_func_{parent.operator.value}_{string_to_hash(str(parent))}"
327
- return function_to_concept(parent, name, namespace, metadata=metadata)
363
+ return function_to_concept(
364
+ parent,
365
+ name,
366
+ metadata=metadata,
367
+ environment=environment,
368
+ namespace=namespace,
369
+ )
328
370
  elif isinstance(parent, ListWrapper):
329
371
  if not name:
330
372
  name = f"{VIRTUAL_CONCEPT_PREFIX}_{string_to_hash(str(parent))}"
@@ -463,7 +463,7 @@ class ParseToObjects(Transformer):
463
463
  datatype=args[2],
464
464
  purpose=args[0],
465
465
  metadata=metadata,
466
- grain=Grain(components=parents),
466
+ grain=Grain(components={x.address for x in parents}),
467
467
  namespace=namespace,
468
468
  keys=parents,
469
469
  modifiers=modifiers,
@@ -530,6 +530,7 @@ class ParseToObjects(Transformer):
530
530
  source_value,
531
531
  name=name,
532
532
  namespace=namespace,
533
+ environment=self.environment,
533
534
  purpose=purpose,
534
535
  metadata=metadata,
535
536
  )
@@ -598,7 +599,7 @@ class ParseToObjects(Transformer):
598
599
  output_purpose=Purpose.CONSTANT,
599
600
  arguments=[constant],
600
601
  ),
601
- grain=Grain(components=[]),
602
+ grain=Grain(components=set()),
602
603
  namespace=namespace,
603
604
  )
604
605
  if concept.metadata:
@@ -623,7 +624,9 @@ class ParseToObjects(Transformer):
623
624
  return args
624
625
 
625
626
  def grain_clause(self, args) -> Grain:
626
- return Grain(components=[self.environment.concepts[a] for a in args[0]])
627
+ return Grain(
628
+ components=set([self.environment.concepts[a].address for a in args[0]])
629
+ )
627
630
 
628
631
  def whole_grain_clause(self, args) -> WholeGrainWrapper:
629
632
  return WholeGrainWrapper(where=args[0])
@@ -715,7 +718,11 @@ class ParseToObjects(Transformer):
715
718
  )
716
719
  elif isinstance(transformation, Function):
717
720
  concept = function_to_concept(
718
- transformation, namespace=namespace, name=output, metadata=metadata
721
+ transformation,
722
+ namespace=namespace,
723
+ name=output,
724
+ metadata=metadata,
725
+ environment=self.environment,
719
726
  )
720
727
  else:
721
728
  raise SyntaxError("Invalid transformation")
@@ -771,10 +778,8 @@ class ParseToObjects(Transformer):
771
778
  def handle_order_item(x, namespace: str):
772
779
  if not isinstance(x, Concept):
773
780
  x = arbitrary_to_concept(
774
- x,
775
- namespace=namespace,
781
+ x, namespace=namespace, environment=self.environment
776
782
  )
777
- self.environment.add_concept(x)
778
783
  return x
779
784
 
780
785
  return [
@@ -1029,12 +1034,32 @@ class ParseToObjects(Transformer):
1029
1034
  order_by=order_by,
1030
1035
  meta=Metadata(line_number=meta.line),
1031
1036
  )
1032
- for parse_pass in [1, 2]:
1037
+ for parse_pass in [
1038
+ 1,
1039
+ 2,
1040
+ ]:
1033
1041
  # the first pass will result in all concepts being defined
1034
1042
  # the second will get grains appropriately
1035
1043
  # eg if someone does sum(x)->a, b+c -> z - we don't know if Z is a key to group by or an aggregate
1036
1044
  # until after the first pass, and so don't know the grain of a
1037
- nselect = []
1045
+
1046
+ if parse_pass == 1:
1047
+ grain = Grain.from_concepts(
1048
+ [
1049
+ x.content
1050
+ for x in output.selection
1051
+ if isinstance(x.content, Concept)
1052
+ ],
1053
+ where_clause=output.where_clause,
1054
+ )
1055
+ if parse_pass == 2:
1056
+ grain = Grain.from_concepts(
1057
+ output.output_components, where_clause=output.where_clause
1058
+ )
1059
+ print(
1060
+ f"end pass {parse_pass} grain {grain} from {output.output_components}"
1061
+ )
1062
+ output.grain = grain
1038
1063
  for item in select_items:
1039
1064
  # we don't know the grain of an aggregate at assignment time
1040
1065
  # so rebuild at this point in the tree
@@ -1064,7 +1089,7 @@ class ParseToObjects(Transformer):
1064
1089
  environment=self.environment,
1065
1090
  )
1066
1091
  output.local_concepts[item.content.address] = item.content
1067
- nselect.append(item)
1092
+
1068
1093
  if order_by:
1069
1094
  output.order_by = order_by.with_select_context(
1070
1095
  local_concepts=output.local_concepts,
@@ -1181,7 +1206,7 @@ class ParseToObjects(Transformer):
1181
1206
  if isinstance(args[0], AggregateWrapper):
1182
1207
  left = arbitrary_to_concept(
1183
1208
  args[0],
1184
- namespace=self.environment.namespace,
1209
+ environment=self.environment,
1185
1210
  )
1186
1211
  self.environment.add_concept(left)
1187
1212
  else:
@@ -1189,7 +1214,7 @@ class ParseToObjects(Transformer):
1189
1214
  if isinstance(args[2], AggregateWrapper):
1190
1215
  right = arbitrary_to_concept(
1191
1216
  args[2],
1192
- namespace=self.environment.namespace,
1217
+ environment=self.environment,
1193
1218
  )
1194
1219
  self.environment.add_concept(right)
1195
1220
  else:
@@ -1227,10 +1252,7 @@ class ParseToObjects(Transformer):
1227
1252
  ):
1228
1253
  right = right.content
1229
1254
  if isinstance(right, (Function, FilterItem, WindowItem, AggregateWrapper)):
1230
- right = arbitrary_to_concept(
1231
- right,
1232
- namespace=self.environment.namespace,
1233
- )
1255
+ right = arbitrary_to_concept(right, environment=self.environment)
1234
1256
  self.environment.add_concept(right, meta=meta)
1235
1257
  return SubselectComparison(
1236
1258
  left=args[0],
@@ -1299,10 +1321,7 @@ class ParseToObjects(Transformer):
1299
1321
  elif isinstance(item, WindowType):
1300
1322
  type = item
1301
1323
  else:
1302
- concept = arbitrary_to_concept(
1303
- item,
1304
- namespace=self.environment.namespace,
1305
- )
1324
+ concept = arbitrary_to_concept(item, environment=self.environment)
1306
1325
  self.environment.add_concept(concept, meta=meta)
1307
1326
  assert concept
1308
1327
  return WindowItem(
trilogy/parsing/render.py CHANGED
@@ -158,7 +158,14 @@ class Renderer:
158
158
 
159
159
  @to_string.register
160
160
  def _(self, arg: "Grain"):
161
- components = ",".join(self.to_string(x) for x in arg.components)
161
+ final = []
162
+ for comp in arg.components:
163
+ if comp.startswith(DEFAULT_NAMESPACE):
164
+ final.append(comp.split(".", 1)[1])
165
+ else:
166
+ final.append(comp)
167
+ final = sorted(final)
168
+ components = ",".join(x for x in final)
162
169
  return f"grain ({components})"
163
170
 
164
171
  @to_string.register