pytrilogy 0.0.3.112__tar.gz → 0.0.3.115__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 (173) hide show
  1. {pytrilogy-0.0.3.112/pytrilogy.egg-info → pytrilogy-0.0.3.115}/PKG-INFO +14 -34
  2. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/README.md +13 -33
  3. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115/pytrilogy.egg-info}/PKG-INFO +14 -34
  4. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/tests/test_functions.py +25 -0
  5. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/tests/test_parse_engine.py +14 -0
  6. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/__init__.py +1 -1
  7. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/constants.py +28 -0
  8. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/enums.py +7 -1
  9. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/functions.py +51 -0
  10. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/models/author.py +10 -2
  11. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/models/build.py +17 -2
  12. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/models/environment.py +1 -0
  13. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/processing/concept_strategies_v3.py +24 -5
  14. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/processing/discovery_node_factory.py +2 -2
  15. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/processing/discovery_utility.py +11 -4
  16. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/processing/node_generators/filter_node.py +7 -0
  17. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/processing/node_generators/unnest_node.py +77 -6
  18. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/statements/author.py +18 -4
  19. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/dialect/base.py +14 -0
  20. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/dialect/duckdb.py +12 -1
  21. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/parsing/common.py +52 -17
  22. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/parsing/parse_engine.py +76 -7
  23. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/parsing/render.py +2 -1
  24. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/parsing/trilogy.lark +15 -5
  25. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/LICENSE.md +0 -0
  26. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/pyproject.toml +0 -0
  27. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/pytrilogy.egg-info/SOURCES.txt +0 -0
  28. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/pytrilogy.egg-info/dependency_links.txt +0 -0
  29. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/pytrilogy.egg-info/entry_points.txt +0 -0
  30. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/pytrilogy.egg-info/requires.txt +0 -0
  31. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/pytrilogy.egg-info/top_level.txt +0 -0
  32. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/requirements.txt +0 -0
  33. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/setup.cfg +0 -0
  34. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/tests/test_datatypes.py +0 -0
  35. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/tests/test_declarations.py +0 -0
  36. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/tests/test_derived_concepts.py +0 -0
  37. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/tests/test_discovery_nodes.py +0 -0
  38. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/tests/test_enums.py +0 -0
  39. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/tests/test_environment.py +0 -0
  40. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/tests/test_execute_models.py +0 -0
  41. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/tests/test_executor.py +0 -0
  42. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/tests/test_failure.py +0 -0
  43. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/tests/test_imports.py +0 -0
  44. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/tests/test_metadata.py +0 -0
  45. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/tests/test_models.py +0 -0
  46. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/tests/test_multi_join_assignments.py +0 -0
  47. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/tests/test_parsing.py +0 -0
  48. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/tests/test_parsing_failures.py +0 -0
  49. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/tests/test_partial_handling.py +0 -0
  50. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/tests/test_query_processing.py +0 -0
  51. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/tests/test_query_render.py +0 -0
  52. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/tests/test_select.py +0 -0
  53. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/tests/test_show.py +0 -0
  54. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/tests/test_statements.py +0 -0
  55. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/tests/test_typing.py +0 -0
  56. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/tests/test_undefined_concept.py +0 -0
  57. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/tests/test_user_functions.py +0 -0
  58. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/tests/test_validators.py +0 -0
  59. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/tests/test_where_clause.py +0 -0
  60. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/ai/__init__.py +0 -0
  61. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/ai/constants.py +0 -0
  62. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/ai/conversation.py +0 -0
  63. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/ai/enums.py +0 -0
  64. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/ai/execute.py +0 -0
  65. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/ai/models.py +0 -0
  66. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/ai/prompts.py +0 -0
  67. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/ai/providers/__init__.py +0 -0
  68. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/ai/providers/anthropic.py +0 -0
  69. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/ai/providers/base.py +0 -0
  70. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/ai/providers/google.py +0 -0
  71. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/ai/providers/openai.py +0 -0
  72. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/ai/providers/utils.py +0 -0
  73. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/authoring/__init__.py +0 -0
  74. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/__init__.py +0 -0
  75. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/constants.py +0 -0
  76. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/env_processor.py +0 -0
  77. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/environment_helpers.py +0 -0
  78. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/ergonomics.py +0 -0
  79. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/exceptions.py +0 -0
  80. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/graph_models.py +0 -0
  81. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/internal.py +0 -0
  82. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/models/__init__.py +0 -0
  83. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/models/build_environment.py +0 -0
  84. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/models/core.py +0 -0
  85. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/models/datasource.py +0 -0
  86. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/models/execute.py +0 -0
  87. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/optimization.py +0 -0
  88. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/optimizations/__init__.py +0 -0
  89. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/optimizations/base_optimization.py +0 -0
  90. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/optimizations/hide_unused_concept.py +0 -0
  91. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/optimizations/inline_datasource.py +0 -0
  92. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/optimizations/predicate_pushdown.py +0 -0
  93. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/processing/__init__.py +0 -0
  94. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/processing/discovery_validation.py +0 -0
  95. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/processing/graph_utils.py +0 -0
  96. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/processing/node_generators/__init__.py +0 -0
  97. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/processing/node_generators/basic_node.py +0 -0
  98. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/processing/node_generators/common.py +0 -0
  99. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/processing/node_generators/constant_node.py +0 -0
  100. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/processing/node_generators/group_node.py +0 -0
  101. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/processing/node_generators/group_to_node.py +0 -0
  102. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/processing/node_generators/multiselect_node.py +0 -0
  103. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/processing/node_generators/node_merge_node.py +0 -0
  104. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/processing/node_generators/recursive_node.py +0 -0
  105. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/processing/node_generators/rowset_node.py +0 -0
  106. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
  107. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +0 -0
  108. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/processing/node_generators/select_merge_node.py +0 -0
  109. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/processing/node_generators/select_node.py +0 -0
  110. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/processing/node_generators/synonym_node.py +0 -0
  111. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/processing/node_generators/union_node.py +0 -0
  112. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/processing/node_generators/window_node.py +0 -0
  113. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/processing/nodes/__init__.py +0 -0
  114. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/processing/nodes/base_node.py +0 -0
  115. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/processing/nodes/filter_node.py +0 -0
  116. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/processing/nodes/group_node.py +0 -0
  117. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/processing/nodes/merge_node.py +0 -0
  118. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/processing/nodes/recursive_node.py +0 -0
  119. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/processing/nodes/select_node_v2.py +0 -0
  120. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/processing/nodes/union_node.py +0 -0
  121. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/processing/nodes/unnest_node.py +0 -0
  122. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/processing/nodes/window_node.py +0 -0
  123. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/processing/utility.py +0 -0
  124. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/query_processor.py +0 -0
  125. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/statements/__init__.py +0 -0
  126. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/statements/build.py +0 -0
  127. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/statements/common.py +0 -0
  128. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/statements/execute.py +0 -0
  129. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/utility.py +0 -0
  130. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/validation/__init__.py +0 -0
  131. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/validation/common.py +0 -0
  132. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/validation/concept.py +0 -0
  133. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/validation/datasource.py +0 -0
  134. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/validation/environment.py +0 -0
  135. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/core/validation/fix.py +0 -0
  136. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/dialect/__init__.py +0 -0
  137. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/dialect/bigquery.py +0 -0
  138. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/dialect/common.py +0 -0
  139. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/dialect/config.py +0 -0
  140. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/dialect/dataframe.py +0 -0
  141. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/dialect/enums.py +0 -0
  142. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/dialect/metadata.py +0 -0
  143. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/dialect/postgres.py +0 -0
  144. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/dialect/presto.py +0 -0
  145. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/dialect/snowflake.py +0 -0
  146. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/dialect/sql_server.py +0 -0
  147. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/engine.py +0 -0
  148. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/executor.py +0 -0
  149. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/hooks/__init__.py +0 -0
  150. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/hooks/base_hook.py +0 -0
  151. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/hooks/graph_hook.py +0 -0
  152. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/hooks/query_debugger.py +0 -0
  153. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/metadata/__init__.py +0 -0
  154. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/parser.py +0 -0
  155. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/parsing/__init__.py +0 -0
  156. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/parsing/config.py +0 -0
  157. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/parsing/exceptions.py +0 -0
  158. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/parsing/helpers.py +0 -0
  159. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/py.typed +0 -0
  160. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/render.py +0 -0
  161. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/scripts/__init__.py +0 -0
  162. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/scripts/trilogy.py +0 -0
  163. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/std/__init__.py +0 -0
  164. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/std/color.preql +0 -0
  165. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/std/date.preql +0 -0
  166. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/std/display.preql +0 -0
  167. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/std/geography.preql +0 -0
  168. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/std/metric.preql +0 -0
  169. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/std/money.preql +0 -0
  170. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/std/net.preql +0 -0
  171. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/std/ranking.preql +0 -0
  172. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/std/report.preql +0 -0
  173. {pytrilogy-0.0.3.112 → pytrilogy-0.0.3.115}/trilogy/utility.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytrilogy
3
- Version: 0.0.3.112
3
+ Version: 0.0.3.115
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Classifier: Programming Language :: Python
6
6
  Classifier: Programming Language :: Python :: 3
@@ -51,61 +51,41 @@ The Trilogy language is an experiment in better SQL for analytics - a streamline
51
51
 
52
52
  Trilogy is especially powerful for data consumption, providing a rich metadata layer that makes creating, interpreting, and visualizing queries easy and expressive.
53
53
 
54
+
55
+ We recommend starting with the studio to explore Trilogy. For integration, `pytrilogy` can be run locally to parse and execute trilogy model [.preql] files using the `trilogy` CLI tool, or can be run in python by importing the `trilogy` package.
56
+
57
+
54
58
  ## Quick Start
55
59
 
56
60
  > [!TIP]
57
61
  > **Try it now:** [Open-source studio](https://trilogydata.dev/trilogy-studio-core/) | [Interactive demo](https://trilogydata.dev/demo/) | [Documentation](https://trilogydata.dev/)
58
62
 
59
- **Install locally:**
63
+ **Install**
60
64
  ```bash
61
65
  pip install pytrilogy
62
66
  ```
63
67
 
64
- **Your first query:**
68
+ **Save in hello.preql**
65
69
  ```sql
66
- # Save as hello.preql
67
- import names;
68
-
69
- const top_names <- ['Elvis', 'Elvira', 'Elrond', 'Sam'];
70
+ const prime <- unnest([2, 3, 5, 7, 11, 13, 17, 19, 23, 29]);
70
71
 
71
- def initcap(word) -> upper(substring(word, 1, 1)) || substring(word, 2, len(word));
72
+ def cube_plus_one(x) -> (x * x * x + 1);
72
73
 
73
74
  WHERE
74
- @initcap(name) in top_names
75
+ prime_cubed_plus_one % 7 = 0
75
76
  SELECT
76
- name,
77
- sum(births) as name_count
77
+ prime,
78
+ @cube_plus_one(prime) as prime_cubed_plus_one
78
79
  ORDER BY
79
- name_count desc
80
+ prime asc
80
81
  LIMIT 10;
81
82
  ```
82
83
 
83
- **Run it:**
84
+ **Run it in DuckDB**
84
85
  ```bash
85
86
  trilogy run hello.preql duckdb
86
87
  ```
87
88
 
88
- We recommend starting with the studio to explore Trilogy. For integration, `pytrilogy` can be run locally to parse and execute trilogy model [.preql] files using the `trilogy` CLI tool, or can be run in python by importing the `trilogy` package.
89
-
90
- ## Trilogy Looks Like SQL
91
-
92
- ```sql
93
- import names;
94
-
95
- const top_names <- ['Elvis', 'Elvira', 'Elrond', 'Sam'];
96
-
97
- def initcap(word) -> upper(substring(word, 1, 1)) || substring(word, 2, len(word));
98
-
99
- WHERE
100
- @initcap(name) in top_names
101
- SELECT
102
- name,
103
- sum(births) as name_count
104
- ORDER BY
105
- name_count desc
106
- LIMIT 10;
107
- ```
108
-
109
89
  ## Trilogy is Easy to Write
110
90
  For humans *and* AI. Enjoy flexible, one-shot query generation without any DB access or security risks.
111
91
 
@@ -19,61 +19,41 @@ The Trilogy language is an experiment in better SQL for analytics - a streamline
19
19
 
20
20
  Trilogy is especially powerful for data consumption, providing a rich metadata layer that makes creating, interpreting, and visualizing queries easy and expressive.
21
21
 
22
+
23
+ We recommend starting with the studio to explore Trilogy. For integration, `pytrilogy` can be run locally to parse and execute trilogy model [.preql] files using the `trilogy` CLI tool, or can be run in python by importing the `trilogy` package.
24
+
25
+
22
26
  ## Quick Start
23
27
 
24
28
  > [!TIP]
25
29
  > **Try it now:** [Open-source studio](https://trilogydata.dev/trilogy-studio-core/) | [Interactive demo](https://trilogydata.dev/demo/) | [Documentation](https://trilogydata.dev/)
26
30
 
27
- **Install locally:**
31
+ **Install**
28
32
  ```bash
29
33
  pip install pytrilogy
30
34
  ```
31
35
 
32
- **Your first query:**
36
+ **Save in hello.preql**
33
37
  ```sql
34
- # Save as hello.preql
35
- import names;
36
-
37
- const top_names <- ['Elvis', 'Elvira', 'Elrond', 'Sam'];
38
+ const prime <- unnest([2, 3, 5, 7, 11, 13, 17, 19, 23, 29]);
38
39
 
39
- def initcap(word) -> upper(substring(word, 1, 1)) || substring(word, 2, len(word));
40
+ def cube_plus_one(x) -> (x * x * x + 1);
40
41
 
41
42
  WHERE
42
- @initcap(name) in top_names
43
+ prime_cubed_plus_one % 7 = 0
43
44
  SELECT
44
- name,
45
- sum(births) as name_count
45
+ prime,
46
+ @cube_plus_one(prime) as prime_cubed_plus_one
46
47
  ORDER BY
47
- name_count desc
48
+ prime asc
48
49
  LIMIT 10;
49
50
  ```
50
51
 
51
- **Run it:**
52
+ **Run it in DuckDB**
52
53
  ```bash
53
54
  trilogy run hello.preql duckdb
54
55
  ```
55
56
 
56
- We recommend starting with the studio to explore Trilogy. For integration, `pytrilogy` can be run locally to parse and execute trilogy model [.preql] files using the `trilogy` CLI tool, or can be run in python by importing the `trilogy` package.
57
-
58
- ## Trilogy Looks Like SQL
59
-
60
- ```sql
61
- import names;
62
-
63
- const top_names <- ['Elvis', 'Elvira', 'Elrond', 'Sam'];
64
-
65
- def initcap(word) -> upper(substring(word, 1, 1)) || substring(word, 2, len(word));
66
-
67
- WHERE
68
- @initcap(name) in top_names
69
- SELECT
70
- name,
71
- sum(births) as name_count
72
- ORDER BY
73
- name_count desc
74
- LIMIT 10;
75
- ```
76
-
77
57
  ## Trilogy is Easy to Write
78
58
  For humans *and* AI. Enjoy flexible, one-shot query generation without any DB access or security risks.
79
59
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: pytrilogy
3
- Version: 0.0.3.112
3
+ Version: 0.0.3.115
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Classifier: Programming Language :: Python
6
6
  Classifier: Programming Language :: Python :: 3
@@ -51,61 +51,41 @@ The Trilogy language is an experiment in better SQL for analytics - a streamline
51
51
 
52
52
  Trilogy is especially powerful for data consumption, providing a rich metadata layer that makes creating, interpreting, and visualizing queries easy and expressive.
53
53
 
54
+
55
+ We recommend starting with the studio to explore Trilogy. For integration, `pytrilogy` can be run locally to parse and execute trilogy model [.preql] files using the `trilogy` CLI tool, or can be run in python by importing the `trilogy` package.
56
+
57
+
54
58
  ## Quick Start
55
59
 
56
60
  > [!TIP]
57
61
  > **Try it now:** [Open-source studio](https://trilogydata.dev/trilogy-studio-core/) | [Interactive demo](https://trilogydata.dev/demo/) | [Documentation](https://trilogydata.dev/)
58
62
 
59
- **Install locally:**
63
+ **Install**
60
64
  ```bash
61
65
  pip install pytrilogy
62
66
  ```
63
67
 
64
- **Your first query:**
68
+ **Save in hello.preql**
65
69
  ```sql
66
- # Save as hello.preql
67
- import names;
68
-
69
- const top_names <- ['Elvis', 'Elvira', 'Elrond', 'Sam'];
70
+ const prime <- unnest([2, 3, 5, 7, 11, 13, 17, 19, 23, 29]);
70
71
 
71
- def initcap(word) -> upper(substring(word, 1, 1)) || substring(word, 2, len(word));
72
+ def cube_plus_one(x) -> (x * x * x + 1);
72
73
 
73
74
  WHERE
74
- @initcap(name) in top_names
75
+ prime_cubed_plus_one % 7 = 0
75
76
  SELECT
76
- name,
77
- sum(births) as name_count
77
+ prime,
78
+ @cube_plus_one(prime) as prime_cubed_plus_one
78
79
  ORDER BY
79
- name_count desc
80
+ prime asc
80
81
  LIMIT 10;
81
82
  ```
82
83
 
83
- **Run it:**
84
+ **Run it in DuckDB**
84
85
  ```bash
85
86
  trilogy run hello.preql duckdb
86
87
  ```
87
88
 
88
- We recommend starting with the studio to explore Trilogy. For integration, `pytrilogy` can be run locally to parse and execute trilogy model [.preql] files using the `trilogy` CLI tool, or can be run in python by importing the `trilogy` package.
89
-
90
- ## Trilogy Looks Like SQL
91
-
92
- ```sql
93
- import names;
94
-
95
- const top_names <- ['Elvis', 'Elvira', 'Elrond', 'Sam'];
96
-
97
- def initcap(word) -> upper(substring(word, 1, 1)) || substring(word, 2, len(word));
98
-
99
- WHERE
100
- @initcap(name) in top_names
101
- SELECT
102
- name,
103
- sum(births) as name_count
104
- ORDER BY
105
- name_count desc
106
- LIMIT 10;
107
- ```
108
-
109
89
  ## Trilogy is Easy to Write
110
90
  For humans *and* AI. Enjoy flexible, one-shot query generation without any DB access or security risks.
111
91
 
@@ -146,6 +146,8 @@ def test_date_functions(test_environment):
146
146
  date_trunc(order_timestamp, hour) -> order_hour_trunc,
147
147
  date_trunc(order_timestamp, minute) -> order_minute_trunc,
148
148
  date_trunc(order_timestamp, second) -> order_second_trunc,
149
+ date_trunc(order_timestamp, quarter) -> order_quarter_trunc,
150
+ date_trunc(order_timestamp, week) -> order_week_trunc,
149
151
  date_part(order_timestamp, month) -> order_month_part,
150
152
  date_part(order_timestamp, day) -> order_day_part,
151
153
  date_part(order_timestamp, year) -> order_year_part,
@@ -155,6 +157,8 @@ def test_date_functions(test_environment):
155
157
  date_part(order_timestamp, quarter) -> order_quarter_part,
156
158
  date_part(order_timestamp, week) -> order_week_part,
157
159
  date_part(order_timestamp, day_of_week) -> order_day_of_week_part,
160
+ month_name(order_timestamp) -> order_month_name,
161
+ day_name(order_timestamp) -> order_day_name
158
162
  ;
159
163
 
160
164
 
@@ -163,7 +167,28 @@ def test_date_functions(test_environment):
163
167
  select: SelectStatement = parsed[-1]
164
168
 
165
169
  for dialect in TEST_DIALECTS:
170
+ engine = None
171
+ if isinstance(dialect, BigqueryDialect):
172
+ engine = Dialects.DUCK_DB.default_executor(environment=test_environment)
166
173
  dialect.compile_statement(process_query(test_environment, select))
174
+ if engine:
175
+ engine.execute_raw_sql(
176
+ """CREATE TABLE tblRevenue AS
177
+ SELECT
178
+ 100.50 as revenue,
179
+ 1 as order_id,
180
+ 101 as product_id,
181
+ TIMESTAMP '2024-01-15 10:30:00' as order_timestamp
182
+ UNION ALL
183
+ SELECT
184
+ 250.75 as revenue,
185
+ 2 as order_id,
186
+ 102 as product_id,
187
+ TIMESTAMP '2024-01-16 14:45:30' as order_timestamp;"""
188
+ )
189
+ results = engine.execute_query(select)
190
+ assert results
191
+ assert results.fetchall()
167
192
 
168
193
 
169
194
  def test_bad_cast(test_environment):
@@ -121,3 +121,17 @@ def test_alias_error():
121
121
  with raises(InvalidSyntaxException) as e:
122
122
  env.parse(TEXT2)
123
123
  assert ERROR_CODES[201] in str(e.value), e.value
124
+
125
+
126
+ def test_semicolon_error():
127
+ env = Environment()
128
+ TEXT2 = """
129
+ const a <- 1;
130
+
131
+ select
132
+ a+2 as fun,
133
+
134
+ """
135
+ with raises(InvalidSyntaxException) as e:
136
+ env.parse(TEXT2)
137
+ assert ERROR_CODES[202] in str(e.value), e.value
@@ -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.112"
7
+ __version__ = "0.0.3.115"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -1,7 +1,9 @@
1
1
  import random
2
+ from contextlib import contextmanager
2
3
  from dataclasses import dataclass, field
3
4
  from enum import Enum
4
5
  from logging import getLogger
6
+ from typing import Any
5
7
 
6
8
  logger = getLogger("trilogy")
7
9
 
@@ -50,6 +52,32 @@ class Rendering:
50
52
  parameters: bool = True
51
53
  concise: bool = False
52
54
 
55
+ @contextmanager
56
+ def temporary(self, **kwargs: Any):
57
+ """
58
+ Context manager to temporarily set attributes and revert them afterwards.
59
+
60
+ Usage:
61
+ r = Rendering()
62
+ with r.temporary(parameters=False, concise=True):
63
+ # parameters is False, concise is True here
64
+ do_something()
65
+ # parameters and concise are back to their original values
66
+ """
67
+ # Store original values
68
+ original_values = {key: getattr(self, key) for key in kwargs}
69
+
70
+ # Set new values
71
+ for key, value in kwargs.items():
72
+ setattr(self, key, value)
73
+
74
+ try:
75
+ yield self
76
+ finally:
77
+ # Restore original values
78
+ for key, value in original_values.items():
79
+ setattr(self, key, value)
80
+
53
81
 
54
82
  @dataclass
55
83
  class Parsing:
@@ -169,6 +169,7 @@ class FunctionType(Enum):
169
169
  ARRAY_SORT = "array_sort"
170
170
  ARRAY_TRANSFORM = "array_transform"
171
171
  ARRAY_TO_STRING = "array_to_string"
172
+ ARRAY_FILTER = "array_filter"
172
173
 
173
174
  # MAP
174
175
  MAP_KEYS = "map_keys"
@@ -204,6 +205,7 @@ class FunctionType(Enum):
204
205
  MIN = "min"
205
206
  AVG = "avg"
206
207
  ARRAY_AGG = "array_agg"
208
+ ANY = "any"
207
209
 
208
210
  # String
209
211
  LIKE = "like"
@@ -236,12 +238,15 @@ class FunctionType(Enum):
236
238
  MONTH = "month"
237
239
  QUARTER = "quarter"
238
240
  YEAR = "year"
241
+ MONTH_NAME = "month_name"
242
+ DAY_NAME = "day_name"
239
243
 
240
244
  DATE_PART = "date_part"
241
245
  DATE_TRUNCATE = "date_truncate"
242
246
  DATE_ADD = "date_add"
243
247
  DATE_SUB = "date_sub"
244
248
  DATE_DIFF = "date_diff"
249
+ DATE_SPINE = "date_spine"
245
250
 
246
251
  # UNIX
247
252
  UNIX_TO_TIMESTAMP = "unix_to_timestamp"
@@ -261,6 +266,7 @@ class FunctionClass(Enum):
261
266
  FunctionType.ARRAY_AGG,
262
267
  FunctionType.COUNT,
263
268
  FunctionType.COUNT_DISTINCT,
269
+ FunctionType.ANY,
264
270
  ]
265
271
  SINGLE_ROW = [
266
272
  FunctionType.CONSTANT,
@@ -268,7 +274,7 @@ class FunctionClass(Enum):
268
274
  FunctionType.CURRENT_DATETIME,
269
275
  ]
270
276
 
271
- ONE_TO_MANY = [FunctionType.UNNEST]
277
+ ONE_TO_MANY = [FunctionType.UNNEST, FunctionType.DATE_SPINE]
272
278
 
273
279
  RECURSIVE = [FunctionType.RECURSE_EDGE]
274
280
 
@@ -175,6 +175,10 @@ def get_date_trunc_output(
175
175
  return DataType.DATETIME
176
176
  elif target == DatePart.SECOND:
177
177
  return DataType.DATETIME
178
+ elif target == DatePart.WEEK:
179
+ return DataType.DATE
180
+ elif target == DatePart.QUARTER:
181
+ return DataType.DATE
178
182
  else:
179
183
  raise InvalidSyntaxException(f"Date truncation not supported for {target}")
180
184
 
@@ -208,6 +212,14 @@ FUNCTION_REGISTRY: dict[FunctionType, FunctionConfig] = {
208
212
  output_type_function=get_unnest_output_type,
209
213
  arg_count=1,
210
214
  ),
215
+ FunctionType.DATE_SPINE: FunctionConfig(
216
+ valid_inputs={
217
+ DataType.DATE,
218
+ },
219
+ output_purpose=Purpose.KEY,
220
+ output_type=DataType.DATE,
221
+ arg_count=2,
222
+ ),
211
223
  FunctionType.RECURSE_EDGE: FunctionConfig(
212
224
  arg_count=2,
213
225
  ),
@@ -314,6 +326,18 @@ FUNCTION_REGISTRY: dict[FunctionType, FunctionConfig] = {
314
326
  output_type_function=get_transform_output_type,
315
327
  arg_count=3,
316
328
  ),
329
+ FunctionType.ARRAY_FILTER: FunctionConfig(
330
+ valid_inputs=[
331
+ {
332
+ DataType.ARRAY,
333
+ },
334
+ {*DataType},
335
+ {*DataType},
336
+ ],
337
+ output_purpose=Purpose.PROPERTY,
338
+ output_type_function=get_transform_output_type,
339
+ arg_count=3,
340
+ ),
317
341
  FunctionType.ARRAY_TO_STRING: FunctionConfig(
318
342
  valid_inputs={
319
343
  DataType.ARRAY,
@@ -640,6 +664,17 @@ FUNCTION_REGISTRY: dict[FunctionType, FunctionConfig] = {
640
664
  output_type=TraitDataType(type=DataType.INTEGER, traits=["day"]),
641
665
  arg_count=1,
642
666
  ),
667
+ FunctionType.DAY_NAME: FunctionConfig(
668
+ valid_inputs={
669
+ DataType.DATE,
670
+ DataType.TIMESTAMP,
671
+ DataType.DATETIME,
672
+ # DataType.STRING,
673
+ },
674
+ output_purpose=Purpose.PROPERTY,
675
+ output_type=TraitDataType(type=DataType.STRING, traits=["day_name"]),
676
+ arg_count=1,
677
+ ),
643
678
  FunctionType.WEEK: FunctionConfig(
644
679
  valid_inputs={
645
680
  DataType.DATE,
@@ -662,6 +697,17 @@ FUNCTION_REGISTRY: dict[FunctionType, FunctionConfig] = {
662
697
  output_type=TraitDataType(type=DataType.INTEGER, traits=["month"]),
663
698
  arg_count=1,
664
699
  ),
700
+ FunctionType.MONTH_NAME: FunctionConfig(
701
+ valid_inputs={
702
+ DataType.DATE,
703
+ DataType.TIMESTAMP,
704
+ DataType.DATETIME,
705
+ # DataType.STRING,
706
+ },
707
+ output_purpose=Purpose.PROPERTY,
708
+ output_type=TraitDataType(type=DataType.STRING, traits=["month_name"]),
709
+ arg_count=1,
710
+ ),
665
711
  FunctionType.QUARTER: FunctionConfig(
666
712
  valid_inputs={
667
713
  DataType.DATE,
@@ -861,6 +907,11 @@ FUNCTION_REGISTRY: dict[FunctionType, FunctionConfig] = {
861
907
  ),
862
908
  arg_count=1,
863
909
  ),
910
+ FunctionType.ANY: FunctionConfig(
911
+ valid_inputs={*DataType},
912
+ output_purpose=Purpose.PROPERTY,
913
+ arg_count=1,
914
+ ),
864
915
  FunctionType.AVG: FunctionConfig(
865
916
  valid_inputs={
866
917
  DataType.INTEGER,
@@ -461,6 +461,11 @@ class WhereClause(Mergeable, ConceptArgs, Namespaced, BaseModel):
461
461
  conditional=self.conditional.with_namespace(namespace)
462
462
  )
463
463
 
464
+ def with_reference_replacement(self, source, target):
465
+ return self.__class__.model_construct(
466
+ conditional=self.conditional.with_reference_replacement(source, target)
467
+ )
468
+
464
469
 
465
470
  class HavingClause(WhereClause):
466
471
  pass
@@ -1212,6 +1217,8 @@ class Concept(Addressable, DataTyped, ConceptArgs, Mergeable, Namespaced, BaseMo
1212
1217
  return Derivation.FILTER
1213
1218
  elif lineage and isinstance(lineage, (BuildAggregateWrapper, AggregateWrapper)):
1214
1219
  return Derivation.AGGREGATE
1220
+ # elif lineage and isinstance(lineage, (BuildParenthetical, Parenthetical)):
1221
+ # return Derivation.PARENTHETICAL
1215
1222
  elif lineage and isinstance(lineage, (BuildRowsetItem, RowsetItem)):
1216
1223
  return Derivation.ROWSET
1217
1224
  elif lineage and isinstance(
@@ -1227,7 +1234,7 @@ class Concept(Addressable, DataTyped, ConceptArgs, Mergeable, Namespaced, BaseMo
1227
1234
  elif (
1228
1235
  lineage
1229
1236
  and isinstance(lineage, (BuildFunction, Function))
1230
- and lineage.operator == FunctionType.UNNEST
1237
+ and lineage.operator in FunctionClass.ONE_TO_MANY.value
1231
1238
  ):
1232
1239
  return Derivation.UNNEST
1233
1240
  elif (
@@ -1279,7 +1286,8 @@ class Concept(Addressable, DataTyped, ConceptArgs, Mergeable, Namespaced, BaseMo
1279
1286
  elif (
1280
1287
  lineage
1281
1288
  and isinstance(lineage, (Function, BuildFunction))
1282
- and lineage.operator in (FunctionType.UNNEST, FunctionType.UNION)
1289
+ and lineage.operator
1290
+ in (FunctionType.UNNEST, FunctionType.UNION, FunctionType.DATE_SPINE)
1283
1291
  ):
1284
1292
  return Granularity.MULTI_ROW
1285
1293
  elif lineage and all(
@@ -134,8 +134,9 @@ def concept_is_relevant(
134
134
  if concept.purpose in (Purpose.METRIC,):
135
135
  if all([c in others for c in concept.grain.components]):
136
136
  return False
137
+ if concept.derivation in (Derivation.UNNEST,):
138
+ return True
137
139
  if concept.derivation in (Derivation.BASIC,):
138
-
139
140
  return any(concept_is_relevant(c, others) for c in concept.concept_arguments)
140
141
  if concept.granularity == Granularity.SINGLE_ROW:
141
142
  return False
@@ -1668,7 +1669,6 @@ class Factory:
1668
1669
  valid_inputs=base.valid_inputs,
1669
1670
  arg_count=base.arg_count,
1670
1671
  )
1671
-
1672
1672
  new = BuildFunction(
1673
1673
  operator=base.operator,
1674
1674
  arguments=[self.handle_constant(self.build(c)) for c in raw_args],
@@ -1724,6 +1724,14 @@ class Factory:
1724
1724
  return self._build_concept(base)
1725
1725
 
1726
1726
  def _build_concept(self, base: Concept) -> BuildConcept:
1727
+ try:
1728
+ return self.__build_concept(base)
1729
+ except RecursionError as e:
1730
+ raise RecursionError(
1731
+ f"Recursion error building concept {base.address}. This is likely due to a circular reference."
1732
+ ) from e
1733
+
1734
+ def __build_concept(self, base: Concept) -> BuildConcept:
1727
1735
  # TODO: if we are using parameters, wrap it in a new model and use that in rendering
1728
1736
  if base.address in self.local_concepts:
1729
1737
  return self.local_concepts[base.address]
@@ -2002,6 +2010,13 @@ class Factory:
2002
2010
  def _build_tuple_wrapper(self, base: TupleWrapper) -> TupleWrapper:
2003
2011
  return TupleWrapper(val=[self.build(x) for x in base.val], type=base.type)
2004
2012
 
2013
+ @build.register
2014
+ def _(self, base: ListWrapper) -> ListWrapper:
2015
+ return self._build_list_wrapper(base)
2016
+
2017
+ def _build_list_wrapper(self, base: ListWrapper) -> ListWrapper:
2018
+ return ListWrapper([self.build(x) for x in base], type=base.type)
2019
+
2005
2020
  @build.register
2006
2021
  def _(self, base: FilterItem) -> BuildFilterItem:
2007
2022
  return self._build_filter_item(base)
@@ -563,6 +563,7 @@ class Environment(BaseModel):
563
563
  existing = self.validate_concept(concept, meta=meta)
564
564
  if existing:
565
565
  concept = existing
566
+
566
567
  self.concepts[concept.address] = concept
567
568
 
568
569
  from trilogy.core.environment_helpers import generate_related_concepts
@@ -306,7 +306,12 @@ def evaluate_loop_conditions(
306
306
 
307
307
 
308
308
  def check_for_early_exit(
309
- complete, partial, missing, context: LoopContext, priority_concept: BuildConcept
309
+ complete: ValidationResult,
310
+ found: set[str],
311
+ partial: set[str],
312
+ missing: set[str],
313
+ context: LoopContext,
314
+ priority_concept: BuildConcept,
310
315
  ) -> bool:
311
316
  if complete == ValidationResult.INCOMPLETE_CONDITION:
312
317
  cond_dict = {str(node): node.preexisting_conditions for node in context.stack}
@@ -331,8 +336,18 @@ def check_for_early_exit(
331
336
  f"{depth_to_prefix(context.depth)}{LOGGER_PREFIX} Breaking as we have attempted all nodes"
332
337
  )
333
338
  return True
339
+ elif all(
340
+ [
341
+ x.address in found and x.address not in partial
342
+ for x in context.mandatory_list
343
+ ]
344
+ ):
345
+ logger.info(
346
+ f"{depth_to_prefix(context.depth)}{LOGGER_PREFIX} Breaking as we have found all mandatory nodes without partials"
347
+ )
348
+ return True
334
349
  logger.info(
335
- f"{depth_to_prefix(context.depth)}{LOGGER_PREFIX} Found complete stack with partials {partial}, continuing search, attempted {context.attempted} all {len(context.mandatory_list)}"
350
+ f"{depth_to_prefix(context.depth)}{LOGGER_PREFIX} Found complete stack with partials {partial}, continuing search, attempted {context.attempted} of total {len(context.mandatory_list)}."
336
351
  )
337
352
  else:
338
353
  logger.info(
@@ -436,6 +451,7 @@ def generate_loop_completion(context: LoopContext, virtual: set[str]) -> Strateg
436
451
  context.original_mandatory,
437
452
  context.environment,
438
453
  non_virtual_difference_values,
454
+ depth=context.depth,
439
455
  )
440
456
 
441
457
  return group_if_required_v2(
@@ -443,6 +459,7 @@ def generate_loop_completion(context: LoopContext, virtual: set[str]) -> Strateg
443
459
  context.original_mandatory,
444
460
  context.environment,
445
461
  non_virtual_difference_values,
462
+ depth=context.depth,
446
463
  )
447
464
 
448
465
 
@@ -466,6 +483,7 @@ def _search_concepts(
466
483
  conditions=conditions,
467
484
  )
468
485
 
486
+ # if we get a can
469
487
  if candidate:
470
488
  return candidate
471
489
  context = initialize_loop_context(
@@ -477,13 +495,14 @@ def _search_concepts(
477
495
  accept_partial=accept_partial,
478
496
  conditions=conditions,
479
497
  )
480
-
498
+ partial: set[str] = set()
481
499
  while context.incomplete:
482
500
 
483
501
  priority_concept = get_priority_concept(
484
502
  context.mandatory_list,
485
503
  context.attempted,
486
504
  found_concepts=context.found,
505
+ partial_concepts=partial,
487
506
  depth=depth,
488
507
  )
489
508
 
@@ -538,7 +557,7 @@ def _search_concepts(
538
557
  # assign
539
558
  context.found = found_c
540
559
  early_exit = check_for_early_exit(
541
- complete, partial, missing_c, context, priority_concept
560
+ complete, found_c, partial, missing_c, context, priority_concept
542
561
  )
543
562
  if early_exit:
544
563
  break
@@ -608,4 +627,4 @@ def source_query_concepts(
608
627
  logger.info(
609
628
  f"{depth_to_prefix(0)}{LOGGER_PREFIX} final concepts are {[x.address for x in final]}"
610
629
  )
611
- return group_if_required_v2(root, output_concepts, environment)
630
+ return group_if_required_v2(root, output_concepts, environment, depth=0)