pytrilogy 0.0.2.33__py3-none-any.whl → 0.0.2.35__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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pytrilogy
3
- Version: 0.0.2.33
3
+ Version: 0.0.2.35
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -20,6 +20,7 @@ Requires-Dist: networkx
20
20
  Requires-Dist: pyodbc
21
21
  Requires-Dist: pydantic
22
22
  Requires-Dist: duckdb-engine
23
+ Requires-Dist: click
23
24
  Provides-Extra: bigquery
24
25
  Requires-Dist: sqlalchemy-bigquery; extra == "bigquery"
25
26
  Provides-Extra: postgres
@@ -1,4 +1,4 @@
1
- trilogy/__init__.py,sha256=a83gD8nszaMaJ-H2JcfjmxDVs4wHl6s8zGL9z5zyk_Y,291
1
+ trilogy/__init__.py,sha256=U5WUYHe0fhxaKEzGbop_KX-WSQsCDldtJFhRs5Ojqys,291
2
2
  trilogy/compiler.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
3
  trilogy/constants.py,sha256=HQAnGUqJ5uMri7TWtqXHhz8iVWBzi2LCfRG8vKnBIB8,1269
4
4
  trilogy/engine.py,sha256=R5ubIxYyrxRExz07aZCUfrTsoXCHQ8DKFTDsobXdWdA,1102
@@ -8,7 +8,7 @@ trilogy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  trilogy/utility.py,sha256=zM__8r29EsyDW7K9VOHz8yvZC2bXFzh7xKy3cL7GKsk,707
9
9
  trilogy/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
10
  trilogy/core/constants.py,sha256=7XaCpZn5mQmjTobbeBn56SzPWq9eMNDfzfsRU-fP0VE,171
11
- trilogy/core/enums.py,sha256=GG51K-bP3HTVuvJQFCjbmVt7iBOGpmF_qtnRVf0_obI,6396
11
+ trilogy/core/enums.py,sha256=z-qBLh0YW52tkRtp6VLn6afxLfgI45r7_QfDtGH9CSw,6858
12
12
  trilogy/core/env_processor.py,sha256=SHVB3nkidIlFc5dz-sofRMKXx66stpLQNuVdQSjC-So,2586
13
13
  trilogy/core/environment_helpers.py,sha256=DIsoo-GcXmXVPB1JbNh8Oku25Nyef9mexPIdy2ur_sk,7159
14
14
  trilogy/core/ergonomics.py,sha256=ASLDd0RqKWrZiG3XcKHo8nyTjaB_8xfE9t4NZ1UvGpc,1639
@@ -16,7 +16,7 @@ trilogy/core/exceptions.py,sha256=NvV_4qLOgKXbpotgRf7c8BANDEvHxlqRPaA53IThQ2o,56
16
16
  trilogy/core/functions.py,sha256=IhVpt3n6wEanKHnGu3oA2w6-hKIlxWpEyz7fHN66mpo,10720
17
17
  trilogy/core/graph_models.py,sha256=mameUTiuCajtihDw_2-W218xyJlvTusOWrEKP1yAWgk,2003
18
18
  trilogy/core/internal.py,sha256=jNGFHKENnbMiMCtAgsnLZYVSENDK4b5ALecXFZpTDzQ,1075
19
- trilogy/core/models.py,sha256=vuy6sGOZ4jI_96wf6bqa7M8CyuD79fKd5lYWkhE0Z98,160393
19
+ trilogy/core/models.py,sha256=9jSrHBh8aE2BD4aoETm_QhRN3KI1mI009xKHTp8fLWE,162668
20
20
  trilogy/core/optimization.py,sha256=VFSvJLNoCCOraip-PZUKeE4qrlxtXARjQUzJZiW-yRk,7325
21
21
  trilogy/core/query_processor.py,sha256=mbcZlgjChrRjDHkdmMbKe-T70UpbBkJhS09MyU5a6UY,17785
22
22
  trilogy/core/optimizations/__init__.py,sha256=bWQecbeiwiDx9LJnLsa7dkWxdbl2wcnkcTN69JyP8iI,356
@@ -70,14 +70,14 @@ trilogy/parsing/common.py,sha256=_GW9LU6_4RuUgcdcr8EE1ybCRd-7cz3idZtjHZ66pYA,101
70
70
  trilogy/parsing/config.py,sha256=Z-DaefdKhPDmSXLgg5V4pebhSB0h590vI0_VtHnlukI,111
71
71
  trilogy/parsing/exceptions.py,sha256=92E5i2frv5hj9wxObJZsZqj5T6bglvPzvdvco_vW1Zk,38
72
72
  trilogy/parsing/helpers.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
73
- trilogy/parsing/parse_engine.py,sha256=N6HnbAru28-uOKrLPKoZnWU7sTboFH8BKFx_TmFFJHA,66519
73
+ trilogy/parsing/parse_engine.py,sha256=ukdJ5QlWcWGkvb7kd3PnnOXEVpURHm0rap8QCvvjALg,63811
74
74
  trilogy/parsing/render.py,sha256=VKyo8wEOuiOzUtJ6w9EoGGmkhlqDQyy8wFj_Q_h6EfE,15263
75
75
  trilogy/parsing/trilogy.lark,sha256=Tuqw5oGMwOYt3TYOEx_hZqGpsAp-PiAKiMW8S3EFRcg,12236
76
76
  trilogy/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
77
77
  trilogy/scripts/trilogy.py,sha256=PHxvv6f2ODv0esyyhWxlARgra8dVhqQhYl0lTrSyVNo,3729
78
- pytrilogy-0.0.2.33.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
79
- pytrilogy-0.0.2.33.dist-info/METADATA,sha256=IFglmRAOBjSrRj19t9GNj1P_sHVNlZRpc3TqQM0lN_c,8403
80
- pytrilogy-0.0.2.33.dist-info/WHEEL,sha256=P9jw-gEje8ByB7_hXoICnHtVCrEwMQh-630tKvQWehc,91
81
- pytrilogy-0.0.2.33.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
82
- pytrilogy-0.0.2.33.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
83
- pytrilogy-0.0.2.33.dist-info/RECORD,,
78
+ pytrilogy-0.0.2.35.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
79
+ pytrilogy-0.0.2.35.dist-info/METADATA,sha256=X18LM4yuJMZ4kh9Q6BUOWlxvEZgmRXMBCp9F3GNmQuA,8424
80
+ pytrilogy-0.0.2.35.dist-info/WHEEL,sha256=a7TGlA-5DaHMRrarXjVbQagU3Man_dCnGIWMJr5kRWo,91
81
+ pytrilogy-0.0.2.35.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
82
+ pytrilogy-0.0.2.35.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
83
+ pytrilogy-0.0.2.35.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (75.3.0)
2
+ Generator: setuptools (75.4.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5
 
trilogy/__init__.py CHANGED
@@ -4,6 +4,6 @@ from trilogy.executor import Executor
4
4
  from trilogy.parser import parse
5
5
  from trilogy.constants import CONFIG
6
6
 
7
- __version__ = "0.0.2.33"
7
+ __version__ = "0.0.2.35"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
trilogy/core/enums.py CHANGED
@@ -215,11 +215,28 @@ class Boolean(Enum):
215
215
  TRUE = "true"
216
216
  FALSE = "false"
217
217
 
218
+ @classmethod
219
+ def _missing_(cls, value):
220
+ if value is True:
221
+ return Boolean.TRUE
222
+ elif value is False:
223
+ return Boolean.FALSE
224
+ strval = str(value)
225
+ if strval.lower() != strval:
226
+ return Boolean(strval.lower())
227
+
218
228
 
219
229
  class BooleanOperator(Enum):
220
230
  AND = "and"
221
231
  OR = "or"
222
232
 
233
+ @classmethod
234
+ def _missing_(cls, value):
235
+ strval = str(value)
236
+ if strval.lower() != strval:
237
+ return BooleanOperator(strval.lower())
238
+ return None
239
+
223
240
 
224
241
  class ComparisonOperator(Enum):
225
242
  LT = "<"
trilogy/core/models.py CHANGED
@@ -1629,6 +1629,45 @@ class SelectStatement(HasUUID, Mergeable, Namespaced, SelectTypeMixin, BaseModel
1629
1629
  self.grain
1630
1630
  )
1631
1631
 
1632
+ def validate_syntax(self):
1633
+ all_in_output = [x.address for x in self.output_components]
1634
+ if self.where_clause:
1635
+ for concept in self.where_clause.concept_arguments:
1636
+
1637
+ if (
1638
+ concept.lineage
1639
+ and isinstance(concept.lineage, Function)
1640
+ and concept.lineage.operator
1641
+ in FunctionClass.AGGREGATE_FUNCTIONS.value
1642
+ ):
1643
+ if concept.address in self.locally_derived:
1644
+ raise SyntaxError(
1645
+ f"Cannot reference an aggregate derived in the select ({concept.address}) in the same statement where clause; move to the HAVING clause instead; Line: {self.meta.line_number}"
1646
+ )
1647
+
1648
+ if (
1649
+ concept.lineage
1650
+ and isinstance(concept.lineage, AggregateWrapper)
1651
+ and concept.lineage.function.operator
1652
+ in FunctionClass.AGGREGATE_FUNCTIONS.value
1653
+ ):
1654
+ if concept.address in self.locally_derived:
1655
+ raise SyntaxError(
1656
+ f"Cannot reference an aggregate derived in the select ({concept.address}) in the same statement where clause; move to the HAVING clause instead; Line: {self.meta.line_number}"
1657
+ )
1658
+ if self.having_clause:
1659
+ for concept in self.having_clause.concept_arguments:
1660
+ if concept.address not in [x.address for x in self.output_components]:
1661
+ raise SyntaxError(
1662
+ f"Cannot reference a column ({concept.address}) that is not in the select projection in the HAVING clause, move to WHERE; Line: {self.meta.line_number}"
1663
+ )
1664
+ if self.order_by:
1665
+ for concept in self.order_by.concept_arguments:
1666
+ if concept.address not in all_in_output:
1667
+ raise SyntaxError(
1668
+ f"Cannot order by a column that is not in the output projection; {self.meta.line_number}"
1669
+ )
1670
+
1632
1671
  def __str__(self):
1633
1672
  from trilogy.parsing.render import render_query
1634
1673
 
@@ -3218,7 +3257,7 @@ class EnvironmentConceptDict(dict):
3218
3257
  def __init__(self, *args, **kwargs) -> None:
3219
3258
  super().__init__(self, *args, **kwargs)
3220
3259
  self.undefined: dict[str, UndefinedConcept] = {}
3221
- self.fail_on_missing: bool = False
3260
+ self.fail_on_missing: bool = True
3222
3261
  self.populate_default_concepts()
3223
3262
 
3224
3263
  def populate_default_concepts(self):
@@ -3460,8 +3499,8 @@ class Environment(BaseModel):
3460
3499
 
3461
3500
  if not exists:
3462
3501
  self.imports[alias].append(imp_stm)
3463
- else:
3464
- return self
3502
+ # we can't exit early
3503
+ # as there may be new concepts
3465
3504
  for k, concept in source.concepts.items():
3466
3505
  if same_namespace:
3467
3506
  new = self.add_concept(concept, _ignore_cache=True)
@@ -3525,8 +3564,11 @@ class Environment(BaseModel):
3525
3564
  try:
3526
3565
  with open(target, "r", encoding="utf-8") as f:
3527
3566
  text = f.read()
3567
+ nenv = Environment(
3568
+ working_path=target.parent,
3569
+ )
3570
+ nenv.concepts.fail_on_missing = False
3528
3571
  nparser = ParseToObjects(
3529
- visit_tokens=True,
3530
3572
  environment=Environment(
3531
3573
  working_path=target.parent,
3532
3574
  ),
@@ -3535,6 +3577,7 @@ class Environment(BaseModel):
3535
3577
  )
3536
3578
  nparser.set_text(text)
3537
3579
  nparser.transform(PARSER.parse(text))
3580
+
3538
3581
  except Exception as e:
3539
3582
  raise ImportError(
3540
3583
  f"Unable to import file {target.parent}, parsing error: {e}"
@@ -30,7 +30,6 @@ from trilogy.core.enums import (
30
30
  WindowType,
31
31
  DatePart,
32
32
  ShowCategory,
33
- FunctionClass,
34
33
  IOType,
35
34
  ConceptSource,
36
35
  )
@@ -227,7 +226,6 @@ def unwrap_transformation(
227
226
  class ParseToObjects(Transformer):
228
227
  def __init__(
229
228
  self,
230
- visit_tokens,
231
229
  environment: Environment,
232
230
  parse_address: str | None = None,
233
231
  token_address: Path | None = None,
@@ -235,7 +233,7 @@ class ParseToObjects(Transformer):
235
233
  tokens: dict[Path | str, ParseTree] | None = None,
236
234
  text_lookup: dict[Path | str, str] | None = None,
237
235
  ):
238
- Transformer.__init__(self, visit_tokens)
236
+ Transformer.__init__(self, True)
239
237
  self.environment: Environment = environment
240
238
  self.parse_address: str = parse_address or SELF_LABEL
241
239
  self.token_address: Path | str = token_address or SELF_LABEL
@@ -254,16 +252,19 @@ class ParseToObjects(Transformer):
254
252
  self.tokens[self.token_address] = tree
255
253
  return results
256
254
 
255
+ def prepare_parse(self):
256
+ self.pass_count = 1
257
+ self.environment.concepts.fail_on_missing = False
258
+ for _, v in self.parsed.items():
259
+ v.prepare_parse()
260
+
257
261
  def hydrate_missing(self):
258
262
  self.pass_count = 2
259
263
  for k, v in self.parsed.items():
260
-
261
264
  if v.pass_count == 2:
262
265
  continue
263
266
  v.hydrate_missing()
264
- # self.environment.concepts.fail_on_missing = True
265
- # if not self.environment.concepts.undefined:
266
- # return self._results_stash
267
+ self.environment.concepts.fail_on_missing = True
267
268
  reparsed = self.transform(self.tokens[self.token_address])
268
269
  self.environment.concepts.undefined = {}
269
270
  return reparsed
@@ -850,12 +851,12 @@ class ParseToObjects(Transformer):
850
851
  nparser = self.parsed[cache_lookup]
851
852
  else:
852
853
  try:
854
+ new_env = Environment(
855
+ working_path=dirname(target),
856
+ )
857
+ new_env.concepts.fail_on_missing = False
853
858
  nparser = ParseToObjects(
854
- visit_tokens=True,
855
- environment=Environment(
856
- working_path=dirname(target),
857
- # namespace=alias,
858
- ),
859
+ environment=new_env,
859
860
  parse_address=cache_lookup,
860
861
  token_address=token_lookup,
861
862
  parsed={**self.parsed, **{self.parse_address: self}},
@@ -985,8 +986,6 @@ class ParseToObjects(Transformer):
985
986
  order_by=order_by,
986
987
  meta=Metadata(line_number=meta.line),
987
988
  )
988
- locally_derived: set[str] = set()
989
- all_in_output: set[str] = set()
990
989
  for item in select_items:
991
990
  # we don't know the grain of an aggregate at assignment time
992
991
  # so rebuild at this point in the tree
@@ -995,25 +994,16 @@ class ParseToObjects(Transformer):
995
994
  new_concept = item.content.output.with_select_context(
996
995
  output.grain,
997
996
  conditional=None,
998
- # conditional=(
999
- # output.where_clause.conditional
1000
- # if output.where_clause
1001
- # and output.where_clause_category == SelectFiltering.IMPLICIT
1002
- # else None
1003
- # ),
1004
997
  environment=self.environment,
1005
998
  )
1006
999
  self.environment.add_concept(new_concept, meta=meta)
1007
1000
  item.content.output = new_concept
1008
- locally_derived.add(new_concept.address)
1009
- all_in_output.add(new_concept.address)
1010
1001
  elif isinstance(item.content, Concept):
1011
1002
  # Sometimes cached values here don't have the latest info
1012
1003
  # but we can't just use environment, as it might not have the right grain.
1013
1004
  item.content = self.environment.concepts[
1014
1005
  item.content.address
1015
1006
  ].with_grain(item.content.grain)
1016
- all_in_output.add(item.content.address)
1017
1007
  if order_by:
1018
1008
  for orderitem in order_by.items:
1019
1009
  if isinstance(orderitem.expr, Concept):
@@ -1021,52 +1011,9 @@ class ParseToObjects(Transformer):
1021
1011
  orderitem.expr = orderitem.expr.with_select_context(
1022
1012
  output.grain,
1023
1013
  conditional=None,
1024
- # conditional=(
1025
- # output.where_clause.conditional
1026
- # if output.where_clause
1027
- # and output.where_clause_category
1028
- # == SelectFiltering.IMPLICIT
1029
- # else None
1030
- # ),
1031
1014
  environment=self.environment,
1032
1015
  )
1033
- if output.where_clause:
1034
- for concept in output.where_clause.concept_arguments:
1035
-
1036
- if (
1037
- concept.lineage
1038
- and isinstance(concept.lineage, Function)
1039
- and concept.lineage.operator
1040
- in FunctionClass.AGGREGATE_FUNCTIONS.value
1041
- ):
1042
- if concept.address in locally_derived:
1043
- raise SyntaxError(
1044
- f"Cannot reference an aggregate derived in the select ({concept.address}) in the same statement where clause; move to the HAVING clause instead; Line: {meta.line}"
1045
- )
1046
-
1047
- if (
1048
- concept.lineage
1049
- and isinstance(concept.lineage, AggregateWrapper)
1050
- and concept.lineage.function.operator
1051
- in FunctionClass.AGGREGATE_FUNCTIONS.value
1052
- ):
1053
- if concept.address in locally_derived:
1054
- raise SyntaxError(
1055
- f"Cannot reference an aggregate derived in the select ({concept.address}) in the same statement where clause; move to the HAVING clause instead; Line: {meta.line}"
1056
- )
1057
- if output.having_clause:
1058
- for concept in output.having_clause.concept_arguments:
1059
- if concept.address not in all_in_output:
1060
- raise SyntaxError(
1061
- f"Cannot reference a column ({concept.address}) that is not in the select projection in the HAVING clause, move to WHERE; Line: {meta.line}"
1062
- )
1063
- if output.order_by:
1064
- for concept in output.order_by.concept_arguments:
1065
- if concept.address not in all_in_output:
1066
- raise SyntaxError(
1067
- f"Cannot order by a column that is not in the output projection; {meta.line}"
1068
- )
1069
-
1016
+ output.validate_syntax()
1070
1017
  return output
1071
1018
 
1072
1019
  @v_args(meta=True)
@@ -1954,12 +1901,14 @@ def parse_text(text: str, environment: Optional[Environment] = None) -> Tuple[
1954
1901
  ],
1955
1902
  ]:
1956
1903
  environment = environment or Environment()
1957
- parser = ParseToObjects(visit_tokens=True, environment=environment)
1904
+ parser = ParseToObjects(environment=environment)
1958
1905
 
1959
1906
  try:
1960
1907
  parser.set_text(text)
1908
+ # disable fail on missing to allow for circular dependencies
1909
+ parser.prepare_parse()
1961
1910
  parser.transform(PARSER.parse(text))
1962
- # handle circular dependencies
1911
+ # this will reset fail on missing
1963
1912
  pass_two = parser.hydrate_missing()
1964
1913
  output = [v for v in pass_two if v]
1965
1914
  except VisitError as e: