pytrilogy 0.0.3.37__tar.gz → 0.0.3.39__tar.gz

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 (140) hide show
  1. {pytrilogy-0.0.3.37/pytrilogy.egg-info → pytrilogy-0.0.3.39}/PKG-INFO +1 -1
  2. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39/pytrilogy.egg-info}/PKG-INFO +1 -1
  3. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/tests/test_models.py +1 -1
  4. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/__init__.py +1 -1
  5. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/environment_helpers.py +59 -42
  6. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/functions.py +6 -4
  7. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/models/author.py +8 -5
  8. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/models/build.py +54 -5
  9. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/models/environment.py +11 -4
  10. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/processing/concept_strategies_v3.py +23 -13
  11. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/processing/node_generators/filter_node.py +3 -2
  12. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/processing/node_generators/group_to_node.py +3 -0
  13. pytrilogy-0.0.3.39/trilogy/core/processing/node_generators/union_node.py +91 -0
  14. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/processing/nodes/__init__.py +19 -0
  15. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/processing/nodes/merge_node.py +3 -0
  16. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/processing/utility.py +30 -6
  17. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/query_processor.py +3 -0
  18. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/statements/author.py +2 -0
  19. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/dialect/base.py +1 -0
  20. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/parsing/common.py +84 -5
  21. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/parsing/parse_engine.py +32 -3
  22. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/parsing/trilogy.lark +41 -19
  23. pytrilogy-0.0.3.37/trilogy/core/processing/node_generators/union_node.py +0 -75
  24. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/LICENSE.md +0 -0
  25. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/README.md +0 -0
  26. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/pyproject.toml +0 -0
  27. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/pytrilogy.egg-info/SOURCES.txt +0 -0
  28. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/pytrilogy.egg-info/dependency_links.txt +0 -0
  29. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/pytrilogy.egg-info/entry_points.txt +0 -0
  30. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/pytrilogy.egg-info/requires.txt +0 -0
  31. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/pytrilogy.egg-info/top_level.txt +0 -0
  32. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/setup.cfg +0 -0
  33. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/setup.py +0 -0
  34. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/tests/test_datatypes.py +0 -0
  35. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/tests/test_declarations.py +0 -0
  36. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/tests/test_derived_concepts.py +0 -0
  37. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/tests/test_discovery_nodes.py +0 -0
  38. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/tests/test_enums.py +0 -0
  39. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/tests/test_environment.py +0 -0
  40. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/tests/test_executor.py +0 -0
  41. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/tests/test_functions.py +0 -0
  42. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/tests/test_imports.py +0 -0
  43. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/tests/test_metadata.py +0 -0
  44. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/tests/test_multi_join_assignments.py +0 -0
  45. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/tests/test_parse_engine.py +0 -0
  46. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/tests/test_parsing.py +0 -0
  47. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/tests/test_partial_handling.py +0 -0
  48. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/tests/test_query_processing.py +0 -0
  49. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/tests/test_query_render.py +0 -0
  50. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/tests/test_select.py +0 -0
  51. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/tests/test_show.py +0 -0
  52. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/tests/test_statements.py +0 -0
  53. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/tests/test_typing.py +0 -0
  54. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/tests/test_undefined_concept.py +0 -0
  55. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/tests/test_user_functions.py +0 -0
  56. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/tests/test_where_clause.py +0 -0
  57. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/authoring/__init__.py +0 -0
  58. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/compiler.py +0 -0
  59. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/constants.py +0 -0
  60. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/__init__.py +0 -0
  61. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/constants.py +0 -0
  62. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/enums.py +0 -0
  63. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/env_processor.py +0 -0
  64. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/ergonomics.py +0 -0
  65. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/exceptions.py +0 -0
  66. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/graph_models.py +0 -0
  67. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/internal.py +0 -0
  68. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/models/__init__.py +0 -0
  69. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/models/build_environment.py +0 -0
  70. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/models/core.py +0 -0
  71. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/models/datasource.py +0 -0
  72. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/models/execute.py +0 -0
  73. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/optimization.py +0 -0
  74. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/optimizations/__init__.py +0 -0
  75. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/optimizations/base_optimization.py +0 -0
  76. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/optimizations/inline_constant.py +0 -0
  77. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/optimizations/inline_datasource.py +0 -0
  78. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/optimizations/predicate_pushdown.py +0 -0
  79. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/processing/__init__.py +0 -0
  80. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/processing/graph_utils.py +0 -0
  81. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/processing/node_generators/__init__.py +0 -0
  82. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/processing/node_generators/basic_node.py +0 -0
  83. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/processing/node_generators/common.py +0 -0
  84. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/processing/node_generators/group_node.py +0 -0
  85. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/processing/node_generators/multiselect_node.py +0 -0
  86. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/processing/node_generators/node_merge_node.py +0 -0
  87. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/processing/node_generators/rowset_node.py +0 -0
  88. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
  89. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +0 -0
  90. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/processing/node_generators/select_merge_node.py +0 -0
  91. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/processing/node_generators/select_node.py +0 -0
  92. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/processing/node_generators/synonym_node.py +0 -0
  93. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/processing/node_generators/unnest_node.py +0 -0
  94. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/processing/node_generators/window_node.py +0 -0
  95. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/processing/nodes/base_node.py +0 -0
  96. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/processing/nodes/filter_node.py +0 -0
  97. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/processing/nodes/group_node.py +0 -0
  98. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/processing/nodes/select_node_v2.py +0 -0
  99. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/processing/nodes/union_node.py +0 -0
  100. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/processing/nodes/unnest_node.py +0 -0
  101. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/processing/nodes/window_node.py +0 -0
  102. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/statements/__init__.py +0 -0
  103. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/statements/build.py +0 -0
  104. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/statements/common.py +0 -0
  105. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/core/statements/execute.py +0 -0
  106. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/dialect/__init__.py +0 -0
  107. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/dialect/bigquery.py +0 -0
  108. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/dialect/common.py +0 -0
  109. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/dialect/config.py +0 -0
  110. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/dialect/dataframe.py +0 -0
  111. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/dialect/duckdb.py +0 -0
  112. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/dialect/enums.py +0 -0
  113. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/dialect/postgres.py +0 -0
  114. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/dialect/presto.py +0 -0
  115. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/dialect/snowflake.py +0 -0
  116. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/dialect/sql_server.py +0 -0
  117. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/engine.py +0 -0
  118. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/executor.py +0 -0
  119. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/hooks/__init__.py +0 -0
  120. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/hooks/base_hook.py +0 -0
  121. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/hooks/graph_hook.py +0 -0
  122. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/hooks/query_debugger.py +0 -0
  123. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/metadata/__init__.py +0 -0
  124. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/parser.py +0 -0
  125. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/parsing/__init__.py +0 -0
  126. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/parsing/config.py +0 -0
  127. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/parsing/exceptions.py +0 -0
  128. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/parsing/helpers.py +0 -0
  129. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/parsing/render.py +0 -0
  130. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/py.typed +0 -0
  131. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/render.py +0 -0
  132. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/scripts/__init__.py +0 -0
  133. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/scripts/trilogy.py +0 -0
  134. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/std/__init__.py +0 -0
  135. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/std/date.preql +0 -0
  136. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/std/display.preql +0 -0
  137. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/std/geography.preql +0 -0
  138. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/std/money.preql +0 -0
  139. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/std/report.preql +0 -0
  140. {pytrilogy-0.0.3.37 → pytrilogy-0.0.3.39}/trilogy/utility.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytrilogy
3
- Version: 0.0.3.37
3
+ Version: 0.0.3.39
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytrilogy
3
- Version: 0.0.3.37
3
+ Version: 0.0.3.39
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -328,7 +328,7 @@ select avg_greater_ten;
328
328
  lineage = env.concepts["avg_greater_ten"].lineage
329
329
  assert isinstance(lineage, AggregateWrapper)
330
330
  assert isinstance(
331
- env.concepts[lineage.function.concept_arguments[0]].lineage, RowsetItem
331
+ env.concepts[lineage.function.concept_arguments[0].address].lineage, RowsetItem
332
332
  )
333
333
 
334
334
 
@@ -4,6 +4,6 @@ from trilogy.dialect.enums import Dialects
4
4
  from trilogy.executor import Executor
5
5
  from trilogy.parser import parse
6
6
 
7
- __version__ = "0.0.3.37"
7
+ __version__ = "0.0.3.39"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -1,10 +1,10 @@
1
1
  from trilogy.constants import DEFAULT_NAMESPACE
2
2
  from trilogy.core.enums import ConceptSource, DatePart, FunctionType, Purpose
3
- from trilogy.core.functions import AttrAccess, FunctionFactory
4
- from trilogy.core.models.author import Concept, Function, Metadata
3
+ from trilogy.core.functions import AttrAccess
4
+ from trilogy.core.models.author import Concept, Function, Metadata, TraitDataType
5
5
  from trilogy.core.models.core import DataType, StructType, arg_to_datatype
6
6
  from trilogy.core.models.environment import Environment
7
- from trilogy.parsing.common import Meta, process_function_args
7
+ from trilogy.parsing.common import Meta
8
8
 
9
9
  FUNCTION_DESCRIPTION_MAPS = {
10
10
  FunctionType.DATE: "The date part of a timestamp/date. Integer, 0-31 depending on month.",
@@ -19,7 +19,6 @@ FUNCTION_DESCRIPTION_MAPS = {
19
19
 
20
20
 
21
21
  def generate_date_concepts(concept: Concept, environment: Environment):
22
- factory = FunctionFactory(environment=environment)
23
22
  if concept.metadata and concept.metadata.description:
24
23
  base_description = concept.metadata.description
25
24
  else:
@@ -28,24 +27,36 @@ def generate_date_concepts(concept: Concept, environment: Environment):
28
27
  base_line_number = concept.metadata.line_number
29
28
  else:
30
29
  base_line_number = None
31
- for ftype in [
32
- FunctionType.MONTH,
33
- FunctionType.YEAR,
34
- FunctionType.QUARTER,
35
- FunctionType.DAY,
36
- FunctionType.DAY_OF_WEEK,
37
- ]:
30
+ arg_tuples: list[tuple[FunctionType, TraitDataType]] = [
31
+ (FunctionType.MONTH, TraitDataType(type=DataType.INTEGER, traits=["month"])),
32
+ (FunctionType.YEAR, TraitDataType(type=DataType.INTEGER, traits=["year"])),
33
+ (
34
+ FunctionType.QUARTER,
35
+ TraitDataType(type=DataType.INTEGER, traits=["quarter"]),
36
+ ),
37
+ (FunctionType.DAY, TraitDataType(type=DataType.INTEGER, traits=["day"])),
38
+ (
39
+ FunctionType.DAY_OF_WEEK,
40
+ TraitDataType(type=DataType.INTEGER, traits=["day_of_week"]),
41
+ ),
42
+ ]
43
+ for ftype, dtype in arg_tuples:
38
44
  fname = ftype.name.lower()
45
+ address = concept.address + f".{fname}"
46
+ if address in environment.concepts:
47
+ continue
39
48
  default_type = (
40
49
  Purpose.CONSTANT
41
50
  if concept.purpose == Purpose.CONSTANT
42
51
  else Purpose.PROPERTY
43
52
  )
44
- function = factory.create_function(
53
+ function = Function.model_construct(
45
54
  operator=ftype,
46
- args=[concept],
55
+ arguments=[concept.reference],
56
+ output_datatype=dtype,
57
+ output_purpose=default_type,
47
58
  )
48
- new_concept = Concept(
59
+ new_concept = Concept.model_construct(
49
60
  name=f"{concept.name}.{fname}",
50
61
  datatype=function.output_datatype,
51
62
  purpose=default_type,
@@ -61,15 +72,19 @@ def generate_date_concepts(concept: Concept, environment: Environment):
61
72
  concept_source=ConceptSource.AUTO_DERIVED,
62
73
  ),
63
74
  )
64
- if new_concept.name in environment.concepts:
65
- continue
66
75
  environment.add_concept(new_concept, add_derived=False)
67
76
  for grain in [DatePart.MONTH, DatePart.YEAR]:
68
- function = factory.create_function(
77
+ address = concept.address + f".{grain.value}_start"
78
+ if address in environment.concepts:
79
+ continue
80
+ function = Function.model_construct(
69
81
  operator=FunctionType.DATE_TRUNCATE,
70
- args=[concept, grain],
82
+ arguments=[concept.reference, grain],
83
+ output_datatype=DataType.DATE,
84
+ output_purpose=default_type,
85
+ arg_count=2,
71
86
  )
72
- new_concept = Concept(
87
+ new_concept = Concept.model_construct(
73
88
  name=f"{concept.name}.{grain.value}_start",
74
89
  datatype=DataType.DATE,
75
90
  purpose=Purpose.PROPERTY,
@@ -85,13 +100,11 @@ def generate_date_concepts(concept: Concept, environment: Environment):
85
100
  concept_source=ConceptSource.AUTO_DERIVED,
86
101
  ),
87
102
  )
88
- if new_concept.name in environment.concepts:
89
- continue
103
+
90
104
  environment.add_concept(new_concept, add_derived=False)
91
105
 
92
106
 
93
107
  def generate_datetime_concepts(concept: Concept, environment: Environment):
94
- factory = FunctionFactory(environment=environment)
95
108
  if concept.metadata and concept.metadata.description:
96
109
  base_description = concept.metadata.description
97
110
  else:
@@ -100,24 +113,29 @@ def generate_datetime_concepts(concept: Concept, environment: Environment):
100
113
  base_line_number = concept.metadata.line_number
101
114
  else:
102
115
  base_line_number = None
103
- for ftype in [
104
- FunctionType.DATE,
105
- FunctionType.HOUR,
106
- FunctionType.MINUTE,
107
- FunctionType.SECOND,
108
- ]:
116
+ setup_tuples: list[tuple[FunctionType, DataType | TraitDataType]] = [
117
+ (FunctionType.DATE, DataType.DATE),
118
+ (FunctionType.HOUR, TraitDataType(type=DataType.INTEGER, traits=["hour"])),
119
+ (FunctionType.MINUTE, TraitDataType(type=DataType.INTEGER, traits=["minute"])),
120
+ (FunctionType.SECOND, TraitDataType(type=DataType.INTEGER, traits=["second"])),
121
+ ]
122
+ for ftype, datatype in setup_tuples:
109
123
  fname = ftype.name.lower()
124
+ address = concept.address + f".{fname}"
125
+ if address in environment.concepts:
126
+ continue
110
127
  default_type = (
111
128
  Purpose.CONSTANT
112
129
  if concept.purpose == Purpose.CONSTANT
113
130
  else Purpose.PROPERTY
114
131
  )
115
-
116
- const_function = factory.create_function(
132
+ const_function = Function.model_construct(
117
133
  operator=ftype,
118
- args=[concept],
134
+ arguments=[concept.reference],
135
+ output_datatype=datatype,
136
+ output_purpose=default_type,
119
137
  )
120
- new_concept = Concept(
138
+ new_concept = Concept.model_construct(
121
139
  name=f"{concept.name}.{fname}",
122
140
  datatype=const_function.output_datatype,
123
141
  purpose=default_type,
@@ -148,15 +166,18 @@ def generate_key_concepts(concept: Concept, environment: Environment):
148
166
  else:
149
167
  base_line_number = None
150
168
  for ftype in [FunctionType.COUNT]:
169
+ address = concept.address + f".{ftype.name.lower()}"
170
+ if address in environment.concepts:
171
+ continue
151
172
  fname = ftype.name.lower()
152
173
  default_type = Purpose.METRIC
153
- const_function: Function = Function(
174
+ const_function: Function = Function.model_construct(
154
175
  operator=ftype,
155
176
  output_datatype=DataType.INTEGER,
156
177
  output_purpose=default_type,
157
- arguments=[concept],
178
+ arguments=[concept.reference],
158
179
  )
159
- new_concept = Concept(
180
+ new_concept = Concept.model_construct(
160
181
  name=f"{concept.name}.{fname}",
161
182
  datatype=DataType.INTEGER,
162
183
  purpose=default_type,
@@ -172,8 +193,6 @@ def generate_key_concepts(concept: Concept, environment: Environment):
172
193
  concept_source=ConceptSource.AUTO_DERIVED,
173
194
  ),
174
195
  )
175
- if new_concept.name in environment.concepts:
176
- continue
177
196
  environment.add_concept(new_concept, add_derived=False)
178
197
 
179
198
 
@@ -199,10 +218,7 @@ def generate_related_concepts(
199
218
 
200
219
  if isinstance(concept.datatype, StructType):
201
220
  for key, value in concept.datatype.fields_map.items():
202
- args = process_function_args(
203
- [concept, key], meta=meta, environment=environment
204
- )
205
- auto = Concept(
221
+ auto = Concept.model_construct(
206
222
  name=key,
207
223
  datatype=arg_to_datatype(value),
208
224
  purpose=Purpose.PROPERTY,
@@ -212,7 +228,8 @@ def generate_related_concepts(
212
228
  and environment.namespace != DEFAULT_NAMESPACE
213
229
  else concept.name
214
230
  ),
215
- lineage=AttrAccess(args, environment=environment),
231
+ lineage=AttrAccess([concept.reference, key], environment=environment),
232
+ grain=concept.grain,
216
233
  )
217
234
  environment.add_concept(auto, meta=meta)
218
235
  if isinstance(value, Concept):
@@ -17,6 +17,7 @@ from trilogy.core.exceptions import InvalidSyntaxException
17
17
  from trilogy.core.models.author import (
18
18
  AggregateWrapper,
19
19
  Concept,
20
+ ConceptRef,
20
21
  Function,
21
22
  Parenthetical,
22
23
  UndefinedConcept,
@@ -35,7 +36,7 @@ from trilogy.core.models.core import (
35
36
  )
36
37
  from trilogy.core.models.environment import Environment
37
38
 
38
- GENERIC_ARGS = Concept | Function | str | int | float | date | datetime
39
+ GENERIC_ARGS = Concept | ConceptRef | Function | str | int | float | date | datetime
39
40
 
40
41
 
41
42
  @dataclass
@@ -191,6 +192,7 @@ FUNCTION_REGISTRY: dict[FunctionType, FunctionConfig] = {
191
192
  ),
192
193
  FunctionType.GROUP: FunctionConfig(
193
194
  arg_count=-1,
195
+ output_type_function=lambda args: get_output_type_at_index(args, 0),
194
196
  ),
195
197
  FunctionType.COUNT: FunctionConfig(
196
198
  output_purpose=Purpose.METRIC,
@@ -772,7 +774,7 @@ class FunctionFactory:
772
774
  output_purpose = Purpose.PROPERTY
773
775
  return Function(
774
776
  operator=operator,
775
- arguments=full_args,
777
+ arguments=full_args, # type: ignore
776
778
  output_datatype=final_output_type,
777
779
  output_purpose=output_purpose,
778
780
  valid_inputs=valid_inputs,
@@ -803,7 +805,7 @@ def create_function_derived_concept(
803
805
  purpose=purpose,
804
806
  lineage=Function(
805
807
  operator=operator,
806
- arguments=arguments,
808
+ arguments=[x.reference for x in arguments],
807
809
  output_datatype=output_type,
808
810
  output_purpose=purpose,
809
811
  arg_count=len(arguments),
@@ -895,7 +897,7 @@ def Split(args: list[Concept], environment: Environment) -> Function:
895
897
  )
896
898
 
897
899
 
898
- def AttrAccess(args: list[GENERIC_ARGS], environment: Environment):
900
+ def AttrAccess(args: list[ConceptRef | str | int], environment: Environment):
899
901
  return FunctionFactory(environment).create_function(
900
902
  args=args, operator=FunctionType.ATTR_ACCESS
901
903
  )
@@ -180,8 +180,14 @@ class UndefinedConcept(ConceptRef):
180
180
 
181
181
  def address_with_namespace(address: str, namespace: str) -> str:
182
182
  existing_ns = address.split(".", 1)[0]
183
+ if "." in address:
184
+ existing_name = address.split(".", 1)[1]
185
+ else:
186
+ existing_name = address
187
+ if existing_name == ALL_ROWS_CONCEPT:
188
+ return address
183
189
  if existing_ns == DEFAULT_NAMESPACE:
184
- return f"{namespace}.{address.split('.',1)[1]}"
190
+ return f"{namespace}.{existing_name}"
185
191
  return f"{namespace}.{address}"
186
192
 
187
193
 
@@ -1129,7 +1135,7 @@ class Concept(Addressable, DataTyped, ConceptArgs, Mergeable, Namespaced, BaseMo
1129
1135
  return self.lineage.concept_arguments if self.lineage else []
1130
1136
 
1131
1137
  @classmethod
1132
- def calculate_derivation(self, lineage, purpose):
1138
+ def calculate_derivation(self, lineage, purpose: Purpose) -> Derivation:
1133
1139
  from trilogy.core.models.build import (
1134
1140
  BuildAggregateWrapper,
1135
1141
  BuildFilterItem,
@@ -1598,9 +1604,6 @@ class Function(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
1598
1604
  ] = None
1599
1605
  arguments: Sequence[FuncArgs]
1600
1606
 
1601
- def __init__(self, **kwargs):
1602
- super().__init__(**kwargs)
1603
-
1604
1607
  def __repr__(self):
1605
1608
  return f'{self.operator.value}({",".join([str(a) for a in self.arguments])})'
1606
1609
 
@@ -65,6 +65,7 @@ from trilogy.core.models.author import (
65
65
  RowsetLineage,
66
66
  SelectLineage,
67
67
  SubselectComparison,
68
+ UndefinedConcept,
68
69
  WhereClause,
69
70
  WindowItem,
70
71
  )
@@ -800,9 +801,6 @@ class BuildConcept(Addressable, BuildConceptArgs, DataTyped, BaseModel):
800
801
  modifiers: List[Modifier] = Field(default_factory=list) # type: ignore
801
802
  pseudonyms: set[str] = Field(default_factory=set)
802
803
 
803
- def with_select_context(self, *args, **kwargs):
804
- return self
805
-
806
804
  @property
807
805
  def is_aggregate(self) -> bool:
808
806
  return self.build_is_aggregate
@@ -1503,7 +1501,7 @@ class Factory:
1503
1501
  return base
1504
1502
 
1505
1503
  @build.register
1506
- def _(self, base: Function) -> BuildFunction:
1504
+ def _(self, base: Function) -> BuildFunction | BuildAggregateWrapper:
1507
1505
  from trilogy.parsing.common import arbitrary_to_concept
1508
1506
 
1509
1507
  raw_args: list[Concept | FuncArgs] = []
@@ -1517,6 +1515,54 @@ class Factory:
1517
1515
  raw_args.append(narg)
1518
1516
  else:
1519
1517
  raw_args.append(arg)
1518
+ if base.operator == FunctionType.GROUP:
1519
+ group_base = raw_args[0]
1520
+ final_args: List[Concept | ConceptRef] = []
1521
+ if isinstance(group_base, ConceptRef):
1522
+ if group_base.address in self.environment.concepts and not isinstance(
1523
+ self.environment.concepts[group_base.address], UndefinedConcept
1524
+ ):
1525
+ group_base = self.environment.concepts[group_base.address]
1526
+ if (
1527
+ isinstance(group_base, Concept)
1528
+ and isinstance(group_base.lineage, AggregateWrapper)
1529
+ and not group_base.lineage.by
1530
+ ):
1531
+ arguments = raw_args[1:]
1532
+ for x in arguments:
1533
+ if isinstance(x, (ConceptRef, Concept)):
1534
+ final_args.append(x)
1535
+ elif isinstance(x, (AggregateWrapper, FilterItem, WindowItem)):
1536
+ newx = arbitrary_to_concept(
1537
+ x,
1538
+ environment=self.environment,
1539
+ )
1540
+ final_args.append(newx)
1541
+ else:
1542
+ # constants, etc, can be ignored for group
1543
+ continue
1544
+ group_base = group_base.model_copy(
1545
+ deep=True,
1546
+ update={
1547
+ "lineage": AggregateWrapper(
1548
+ function=group_base.lineage.function,
1549
+ by=final_args,
1550
+ )
1551
+ },
1552
+ )
1553
+ group_base = group_base.with_grain(
1554
+ Grain.from_concepts(final_args, environment=self.environment)
1555
+ )
1556
+ rval = self.build(group_base)
1557
+ return BuildFunction.model_construct(
1558
+ operator=base.operator,
1559
+ arguments=[rval, *[self.build(c) for c in raw_args[1:]]],
1560
+ output_datatype=base.output_datatype,
1561
+ output_purpose=base.output_purpose,
1562
+ valid_inputs=base.valid_inputs,
1563
+ arg_count=base.arg_count,
1564
+ )
1565
+
1520
1566
  new = BuildFunction.model_construct(
1521
1567
  operator=base.operator,
1522
1568
  arguments=[self.build(c) for c in raw_args],
@@ -1568,7 +1614,10 @@ class Factory:
1568
1614
  new_lineage, final_grain, _ = base.get_select_grain_and_keys(
1569
1615
  self.grain, self.environment
1570
1616
  )
1571
- build_lineage = self.build(new_lineage)
1617
+ if new_lineage:
1618
+ build_lineage = self.build(new_lineage)
1619
+ else:
1620
+ build_lineage = None
1572
1621
  derivation = Concept.calculate_derivation(build_lineage, base.purpose)
1573
1622
  granularity = Concept.calculate_granularity(
1574
1623
  derivation, final_grain, build_lineage
@@ -129,6 +129,9 @@ class EnvironmentConceptDict(dict):
129
129
  def __getitem__(
130
130
  self, key: str, line_no: int | None = None, file: Path | None = None
131
131
  ) -> Concept | UndefinedConceptFull:
132
+ # fast access path
133
+ if key in self.keys():
134
+ return super(EnvironmentConceptDict, self).__getitem__(key)
132
135
  if isinstance(key, ConceptRef):
133
136
  return self.__getitem__(key.address, line_no=line_no, file=file)
134
137
  try:
@@ -311,11 +314,15 @@ class Environment(BaseModel):
311
314
  f.write(self.model_dump_json())
312
315
  return ppath
313
316
 
314
- def validate_concept(self, new_concept: Concept, meta: Meta | None = None):
317
+ def validate_concept(
318
+ self, new_concept: Concept, meta: Meta | None = None
319
+ ) -> Concept | None:
315
320
  lookup = new_concept.address
321
+ if lookup not in self.concepts:
322
+ return None
316
323
  existing: Concept = self.concepts.get(lookup) # type: ignore
317
- if not existing or isinstance(existing, UndefinedConcept):
318
- return
324
+ if isinstance(existing, UndefinedConcept):
325
+ return None
319
326
 
320
327
  def handle_persist():
321
328
  deriv_lookup = (
@@ -361,7 +368,7 @@ class Environment(BaseModel):
361
368
  if existing and self.config.allow_duplicate_declaration:
362
369
  if existing.metadata.concept_source == ConceptSource.PERSIST_STATEMENT:
363
370
  return handle_persist()
364
- return
371
+ return None
365
372
  elif existing.metadata:
366
373
  if existing.metadata.concept_source == ConceptSource.PERSIST_STATEMENT:
367
374
  return handle_persist()
@@ -110,11 +110,14 @@ def get_priority_concept(
110
110
  depth: int,
111
111
  ) -> BuildConcept:
112
112
  # optimized search for missing concepts
113
- pass_one = [
114
- c
115
- for c in all_concepts
116
- if c.address not in attempted_addresses and c.address not in found_concepts
117
- ]
113
+ pass_one = sorted(
114
+ [
115
+ c
116
+ for c in all_concepts
117
+ if c.address not in attempted_addresses and c.address not in found_concepts
118
+ ],
119
+ key=lambda x: x.address,
120
+ )
118
121
  # sometimes we need to scan intermediate concepts to get merge keys or filter keys,
119
122
  # so do an exhaustive search
120
123
  # pass_two = [c for c in all_concepts+filter_only if c.address not in attempted_addresses]
@@ -388,6 +391,9 @@ def generate_node(
388
391
  # conditions=conditions,
389
392
  )
390
393
  else:
394
+ logger.info(
395
+ f"{depth_to_prefix(depth)}{LOGGER_PREFIX} skipping search, already in a recursion fot these concepts"
396
+ )
391
397
  return None
392
398
  return ConstantNode(
393
399
  input_concepts=[],
@@ -453,9 +459,10 @@ def generate_node(
453
459
  f"{depth_to_prefix(depth)}{LOGGER_PREFIX} including any filters, there are non-root concepts we should expand first: {non_root}. Recursing with all of these as mandatory"
454
460
  )
455
461
 
456
- if not history.check_started(
457
- root_targets, accept_partial=accept_partial, conditions=conditions
458
- ):
462
+ # if not history.check_started(
463
+ # root_targets, accept_partial=accept_partial, conditions=conditions
464
+ # ) or 1==1:
465
+ if True:
459
466
  history.log_start(
460
467
  root_targets, accept_partial=accept_partial, conditions=conditions
461
468
  )
@@ -470,7 +477,10 @@ def generate_node(
470
477
  # which we do whenever we hit a root node
471
478
  # conditions=conditions,
472
479
  )
473
-
480
+ else:
481
+ logger.info(
482
+ f"{depth_to_prefix(depth)}{LOGGER_PREFIX} skipping root search, already in a recursion for these concepts"
483
+ )
474
484
  check = history.gen_select_node(
475
485
  concept,
476
486
  local_optional,
@@ -546,7 +556,10 @@ def generate_node(
546
556
  f"{depth_to_prefix(depth)}{LOGGER_PREFIX} resolved concepts through synonyms"
547
557
  )
548
558
  return resolved
549
-
559
+ else:
560
+ logger.info(
561
+ f"{depth_to_prefix(depth)}{LOGGER_PREFIX} skipping synonym search, already in a recursion for these concepts"
562
+ )
550
563
  return None
551
564
  else:
552
565
  raise ValueError(f"Unknown derivation {concept.derivation} on {concept}")
@@ -1041,7 +1054,6 @@ def _search_concepts(
1041
1054
  environment=environment,
1042
1055
  depth=depth,
1043
1056
  )
1044
- logger.info(f"gcheck result is {result}")
1045
1057
  if result.required:
1046
1058
  logger.info(
1047
1059
  f"{depth_to_prefix(depth)}{LOGGER_PREFIX} Adding group node"
@@ -1104,7 +1116,6 @@ def source_query_concepts(
1104
1116
  history=history,
1105
1117
  conditions=conditions,
1106
1118
  )
1107
-
1108
1119
  if not root:
1109
1120
  error_strings = [
1110
1121
  f"{c.address}<{c.purpose}>{c.derivation}>" for c in output_concepts
@@ -1130,5 +1141,4 @@ def source_query_concepts(
1130
1141
  )
1131
1142
  else:
1132
1143
  candidate = root
1133
-
1134
1144
  return candidate
@@ -48,9 +48,10 @@ def gen_filter_node(
48
48
  if isinstance(x.lineage, FILTER_TYPES):
49
49
  if concept.lineage.where == where:
50
50
  logger.info(
51
- f"{padding(depth)}{LOGGER_PREFIX} fetching {x.lineage.content.address} as optional parent with same filter conditions "
51
+ f"{padding(depth)}{LOGGER_PREFIX} fetching {x.lineage.content.address} as optional parent from optional {x} with same filter conditions "
52
52
  )
53
- parent_row_concepts.append(x.lineage.content)
53
+ if x.lineage.content.address not in parent_row_concepts:
54
+ parent_row_concepts.append(x.lineage.content)
54
55
  optional_included.append(x)
55
56
  continue
56
57
  if conditions and conditions == where:
@@ -3,6 +3,8 @@ from typing import List
3
3
  from trilogy.constants import logger
4
4
  from trilogy.core.models.build import BuildConcept, BuildFunction, BuildWhereClause
5
5
  from trilogy.core.models.build_environment import BuildEnvironment
6
+
7
+ # C:\Users\ethan\coding_projects\pytrilogy\trilogy\core\processing\node_generators\group_to_node.py
6
8
  from trilogy.core.processing.nodes import (
7
9
  GroupNode,
8
10
  History,
@@ -30,6 +32,7 @@ def gen_group_to_node(
30
32
  f"Group to should have function lineage, is {type(concept.lineage)}"
31
33
  )
32
34
  group_arg = concept.lineage.arguments[0]
35
+
33
36
  parent_concepts: List[BuildConcept] = concept.lineage.concept_arguments
34
37
  logger.info(
35
38
  f"{padding(depth)}{LOGGER_PREFIX} group by node has required parents {[x.address for x in parent_concepts]}"
@@ -0,0 +1,91 @@
1
+ from typing import List
2
+
3
+ from trilogy.constants import logger
4
+ from trilogy.core.enums import FunctionType
5
+ from trilogy.core.models.build import BuildConcept, BuildFunction, BuildWhereClause
6
+ from trilogy.core.processing.nodes import History, StrategyNode, UnionNode
7
+ from trilogy.core.processing.utility import padding
8
+
9
+ LOGGER_PREFIX = "[GEN_UNION_NODE]"
10
+
11
+
12
+ def is_union(c: BuildConcept):
13
+ return (
14
+ isinstance(c.lineage, BuildFunction)
15
+ and c.lineage.operator == FunctionType.UNION
16
+ )
17
+
18
+
19
+ def build_layers(
20
+ concepts: list[BuildConcept],
21
+ ) -> tuple[list[list[BuildConcept]], list[BuildConcept]]:
22
+ sources = {
23
+ x.address: x.lineage.concept_arguments if x.lineage else [] for x in concepts
24
+ }
25
+ root = concepts[0]
26
+
27
+ built_layers = []
28
+ layers = root.lineage.concept_arguments if root.lineage else []
29
+ sourced = set()
30
+ while layers:
31
+ layer = []
32
+ current = layers.pop()
33
+ sourced.add(current.address)
34
+ layer.append(current)
35
+ for key, values in sources.items():
36
+ if key == current.address:
37
+ continue
38
+ for value in values:
39
+ if value.address in (current.keys or []) or current.address in (
40
+ value.keys or []
41
+ ):
42
+ layer.append(value)
43
+ sourced.add(value.address)
44
+ built_layers.append(layer)
45
+ complete = [
46
+ x for x in concepts if all([x.address in sourced for x in sources[x.address]])
47
+ ]
48
+ return built_layers, complete
49
+
50
+
51
+ def gen_union_node(
52
+ concept: BuildConcept,
53
+ local_optional: List[BuildConcept],
54
+ environment,
55
+ g,
56
+ depth: int,
57
+ source_concepts,
58
+ history: History | None = None,
59
+ conditions: BuildWhereClause | None = None,
60
+ ) -> StrategyNode | None:
61
+ all_unions = [x for x in local_optional if is_union(x)] + [concept]
62
+ logger.info(f"{padding(depth)}{LOGGER_PREFIX} found unions {all_unions}")
63
+ parent_nodes = []
64
+ layers, resolved = build_layers(all_unions)
65
+ for layer in layers:
66
+ logger.info(
67
+ f"{padding(depth)}{LOGGER_PREFIX} fetching layer {layer} with resolved {resolved}"
68
+ )
69
+ parent: StrategyNode = source_concepts(
70
+ mandatory_list=layer,
71
+ environment=environment,
72
+ g=g,
73
+ depth=depth + 1,
74
+ history=history,
75
+ conditions=conditions,
76
+ )
77
+ parent.hide_output_concepts(parent.output_concepts)
78
+ parent.add_output_concepts(resolved)
79
+ parent_nodes.append(parent)
80
+ if not parent:
81
+ logger.info(
82
+ f"{padding(depth)}{LOGGER_PREFIX} could not find union node parents"
83
+ )
84
+ return None
85
+
86
+ return UnionNode(
87
+ input_concepts=resolved,
88
+ output_concepts=resolved,
89
+ environment=environment,
90
+ parents=parent_nodes,
91
+ )