pytrilogy 0.0.2.48__py3-none-any.whl → 0.0.2.50__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of pytrilogy might be problematic. Click here for more details.

Files changed (38) hide show
  1. {pytrilogy-0.0.2.48.dist-info → pytrilogy-0.0.2.50.dist-info}/METADATA +1 -1
  2. {pytrilogy-0.0.2.48.dist-info → pytrilogy-0.0.2.50.dist-info}/RECORD +38 -38
  3. trilogy/__init__.py +1 -1
  4. trilogy/core/enums.py +11 -0
  5. trilogy/core/functions.py +4 -1
  6. trilogy/core/models.py +29 -14
  7. trilogy/core/processing/concept_strategies_v3.py +3 -3
  8. trilogy/core/processing/node_generators/common.py +0 -2
  9. trilogy/core/processing/node_generators/filter_node.py +0 -3
  10. trilogy/core/processing/node_generators/group_node.py +0 -1
  11. trilogy/core/processing/node_generators/group_to_node.py +0 -2
  12. trilogy/core/processing/node_generators/multiselect_node.py +0 -2
  13. trilogy/core/processing/node_generators/node_merge_node.py +0 -1
  14. trilogy/core/processing/node_generators/rowset_node.py +27 -8
  15. trilogy/core/processing/node_generators/select_merge_node.py +138 -59
  16. trilogy/core/processing/node_generators/union_node.py +0 -1
  17. trilogy/core/processing/node_generators/unnest_node.py +0 -2
  18. trilogy/core/processing/node_generators/window_node.py +0 -2
  19. trilogy/core/processing/nodes/base_node.py +28 -3
  20. trilogy/core/processing/nodes/filter_node.py +0 -3
  21. trilogy/core/processing/nodes/group_node.py +9 -6
  22. trilogy/core/processing/nodes/merge_node.py +3 -4
  23. trilogy/core/processing/nodes/select_node_v2.py +5 -4
  24. trilogy/core/processing/nodes/union_node.py +0 -3
  25. trilogy/core/processing/nodes/unnest_node.py +0 -3
  26. trilogy/core/processing/nodes/window_node.py +0 -3
  27. trilogy/core/processing/utility.py +4 -1
  28. trilogy/core/query_processor.py +3 -8
  29. trilogy/dialect/base.py +14 -2
  30. trilogy/dialect/duckdb.py +7 -0
  31. trilogy/hooks/graph_hook.py +14 -0
  32. trilogy/parsing/common.py +14 -5
  33. trilogy/parsing/parse_engine.py +32 -0
  34. trilogy/parsing/trilogy.lark +3 -1
  35. {pytrilogy-0.0.2.48.dist-info → pytrilogy-0.0.2.50.dist-info}/LICENSE.md +0 -0
  36. {pytrilogy-0.0.2.48.dist-info → pytrilogy-0.0.2.50.dist-info}/WHEEL +0 -0
  37. {pytrilogy-0.0.2.48.dist-info → pytrilogy-0.0.2.50.dist-info}/entry_points.txt +0 -0
  38. {pytrilogy-0.0.2.48.dist-info → pytrilogy-0.0.2.50.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,5 @@
1
1
  from dataclasses import dataclass
2
+ from datetime import date, datetime
2
3
  from enum import Enum
3
4
  from logging import Logger
4
5
  from typing import Any, Dict, List, Set, Tuple
@@ -214,7 +215,7 @@ def concept_to_relevant_joins(concepts: list[Concept]) -> List[Concept]:
214
215
  x for x in concepts if x.keys and all([key in addresses for key in x.keys])
215
216
  ]
216
217
  )
217
- final = [c for c in concepts if c not in sub_props]
218
+ final = [c for c in concepts if c.address not in sub_props]
218
219
  return unique(final, "address")
219
220
 
220
221
 
@@ -380,6 +381,8 @@ def is_scalar_condition(
380
381
  int
381
382
  | str
382
383
  | float
384
+ | date
385
+ | datetime
383
386
  | list[Any]
384
387
  | WindowItem
385
388
  | FilterItem
@@ -7,7 +7,6 @@ from trilogy.core.constants import CONSTANT_DATASET
7
7
  from trilogy.core.enums import BooleanOperator, SourceType
8
8
  from trilogy.core.env_processor import generate_graph
9
9
  from trilogy.core.ergonomics import generate_cte_names
10
- from trilogy.core.graph_models import ReferenceGraph
11
10
  from trilogy.core.models import (
12
11
  CTE,
13
12
  BaseJoin,
@@ -353,13 +352,12 @@ def datasource_to_cte(
353
352
  def get_query_node(
354
353
  environment: Environment,
355
354
  statement: SelectStatement | MultiSelectStatement,
356
- graph: Optional[ReferenceGraph] = None,
357
355
  history: History | None = None,
358
356
  ) -> StrategyNode:
359
357
  environment = environment.duplicate()
360
358
  for k, v in statement.local_concepts.items():
361
359
  environment.concepts[k] = v
362
- graph = graph or generate_graph(environment)
360
+ graph = generate_graph(environment)
363
361
  logger.info(
364
362
  f"{LOGGER_PREFIX} getting source datasource for query with filtering {statement.where_clause_category} and output {[str(c) for c in statement.output_components]}"
365
363
  )
@@ -393,7 +391,6 @@ def get_query_node(
393
391
  input_concepts=ds.output_concepts,
394
392
  parents=[ds],
395
393
  environment=ds.environment,
396
- g=ds.g,
397
394
  partial_concepts=ds.partial_concepts,
398
395
  conditions=final,
399
396
  )
@@ -403,11 +400,10 @@ def get_query_node(
403
400
  def get_query_datasources(
404
401
  environment: Environment,
405
402
  statement: SelectStatement | MultiSelectStatement,
406
- graph: Optional[ReferenceGraph] = None,
407
403
  hooks: Optional[List[BaseHook]] = None,
408
404
  ) -> QueryDatasource:
409
405
 
410
- ds = get_query_node(environment, statement, graph)
406
+ ds = get_query_node(environment, statement)
411
407
  final_qds = ds.resolve()
412
408
  if hooks:
413
409
  for hook in hooks:
@@ -479,10 +475,9 @@ def process_query(
479
475
  hooks: List[BaseHook] | None = None,
480
476
  ) -> ProcessedQuery:
481
477
  hooks = hooks or []
482
- graph = generate_graph(environment)
483
478
 
484
479
  root_datasource = get_query_datasources(
485
- environment=environment, graph=graph, statement=statement, hooks=hooks
480
+ environment=environment, statement=statement, hooks=hooks
486
481
  )
487
482
  for hook in hooks:
488
483
  hook.process_root_datasource(root_datasource)
trilogy/dialect/base.py CHANGED
@@ -1,3 +1,4 @@
1
+ from datetime import date, datetime
1
2
  from typing import Any, Callable, Dict, List, Optional, Sequence, Union
2
3
 
3
4
  from jinja2 import Template
@@ -102,13 +103,15 @@ WINDOW_FUNCTION_MAP = {
102
103
  WindowType.AVG: window_factory("avg", include_concept=True),
103
104
  }
104
105
 
105
- DATATYPE_MAP = {
106
+ DATATYPE_MAP: dict[DataType, str] = {
106
107
  DataType.STRING: "string",
107
108
  DataType.INTEGER: "int",
108
109
  DataType.FLOAT: "float",
109
110
  DataType.BOOL: "bool",
110
111
  DataType.NUMERIC: "numeric",
111
112
  DataType.MAP: "map",
113
+ DataType.DATE: "date",
114
+ DataType.DATETIME: "datetime",
112
115
  }
113
116
 
114
117
 
@@ -131,6 +134,7 @@ FUNCTION_MAP = {
131
134
  FunctionType.SPLIT: lambda x: f"split({x[0]}, {x[1]})",
132
135
  FunctionType.IS_NULL: lambda x: f"isnull({x[0]})",
133
136
  FunctionType.BOOL: lambda x: f"CASE WHEN {x[0]} THEN TRUE ELSE FALSE END",
137
+ FunctionType.PARENTHETICAL: lambda x: f"({x[0]})",
134
138
  # Complex
135
139
  FunctionType.INDEX_ACCESS: lambda x: f"{x[0]}[{x[1]}]",
136
140
  FunctionType.MAP_ACCESS: lambda x: f"{x[0]}[{x[1]}][1]",
@@ -138,6 +142,8 @@ FUNCTION_MAP = {
138
142
  FunctionType.ATTR_ACCESS: lambda x: f"""{x[0]}.{x[1].replace("'", "")}""",
139
143
  FunctionType.STRUCT: lambda x: f"{{{', '.join(struct_arg(x))}}}",
140
144
  FunctionType.ARRAY: lambda x: f"[{', '.join(x)}]",
145
+ FunctionType.DATE_LITERAL: lambda x: f"date '{x}'",
146
+ FunctionType.DATETIME_LITERAL: lambda x: f"datetime '{x}'",
141
147
  # math
142
148
  FunctionType.ADD: lambda x: " + ".join(x),
143
149
  FunctionType.SUBTRACT: lambda x: " - ".join(x),
@@ -454,6 +460,8 @@ class BaseDialect:
454
460
  list,
455
461
  bool,
456
462
  float,
463
+ date,
464
+ datetime,
457
465
  DataType,
458
466
  Function,
459
467
  Parenthetical,
@@ -612,7 +620,7 @@ class BaseDialect:
612
620
  elif isinstance(e, list):
613
621
  return f"{self.FUNCTION_MAP[FunctionType.ARRAY]([self.render_expr(x, cte=cte, cte_map=cte_map, raise_invalid=raise_invalid) for x in e])}"
614
622
  elif isinstance(e, DataType):
615
- return str(e.value)
623
+ return self.DATATYPE_MAP.get(e, e.value)
616
624
  elif isinstance(e, DatePart):
617
625
  return str(e.value)
618
626
  elif isinstance(e, NumericType):
@@ -620,6 +628,10 @@ class BaseDialect:
620
628
  elif isinstance(e, MagicConstants):
621
629
  if e == MagicConstants.NULL:
622
630
  return "null"
631
+ elif isinstance(e, date):
632
+ return self.FUNCTION_MAP[FunctionType.DATE_LITERAL](e)
633
+ elif isinstance(e, datetime):
634
+ return self.FUNCTION_MAP[FunctionType.DATETIME_LITERAL](e)
623
635
  else:
624
636
  raise ValueError(f"Unable to render type {type(e)} {e}")
625
637
 
trilogy/dialect/duckdb.py CHANGED
@@ -3,6 +3,7 @@ from typing import Any, Callable, Mapping
3
3
  from jinja2 import Template
4
4
 
5
5
  from trilogy.core.enums import FunctionType, UnnestMode, WindowType
6
+ from trilogy.core.models import DataType
6
7
  from trilogy.dialect.base import BaseDialect
7
8
 
8
9
  WINDOW_FUNCTION_MAP: Mapping[WindowType, Callable[[Any, Any, Any], str]] = {}
@@ -30,6 +31,8 @@ FUNCTION_MAP = {
30
31
  FunctionType.DATE_PART: lambda x: f"date_part('{x[1]}', {x[0]})",
31
32
  FunctionType.DATE_DIFF: lambda x: f"date_diff('{x[2]}', {x[0]}, {x[1]})",
32
33
  FunctionType.CONCAT: lambda x: f"({' || '.join(x)})",
34
+ FunctionType.DATE_LITERAL: lambda x: f"date '{x}'",
35
+ FunctionType.DATETIME_LITERAL: lambda x: f"datetime '{x}'",
33
36
  }
34
37
 
35
38
  # if an aggregate function is called on a source that is at the same grain as the aggregate
@@ -44,6 +47,9 @@ FUNCTION_GRAIN_MATCH_MAP = {
44
47
  FunctionType.MIN: lambda args: f"{args[0]}",
45
48
  }
46
49
 
50
+ DATATYPE_MAP: dict[DataType, str] = {}
51
+
52
+
47
53
  DUCKDB_TEMPLATE = Template(
48
54
  """{%- if output %}
49
55
  CREATE OR REPLACE TABLE {{ output.address.location }} AS
@@ -84,6 +90,7 @@ class DuckDBDialect(BaseDialect):
84
90
  **BaseDialect.FUNCTION_GRAIN_MATCH_MAP,
85
91
  **FUNCTION_GRAIN_MATCH_MAP,
86
92
  }
93
+ DATATYPE_MAP = {**BaseDialect.DATATYPE_MAP, **DATATYPE_MAP}
87
94
  QUOTE_CHARACTER = '"'
88
95
  SQL_TEMPLATE = DUCKDB_TEMPLATE
89
96
  UNNEST_MODE = UnnestMode.DIRECT
@@ -1,7 +1,19 @@
1
+ import sys
2
+ from os import environ
3
+
1
4
  import networkx as nx
2
5
 
3
6
  from trilogy.hooks.base_hook import BaseHook
4
7
 
8
+ if not environ.get("TCL_LIBRARY"):
9
+ minor = sys.version_info.minor
10
+ if minor == 13:
11
+ environ["TCL_LIBRARY"] = r"C:\Program Files\Python313\tcl\tcl8.6"
12
+ elif minor == 12:
13
+ environ["TCL_LIBRARY"] = r"C:\Program Files\Python312\tcl\tcl8.6"
14
+ else:
15
+ pass
16
+
5
17
 
6
18
  class GraphHook(BaseHook):
7
19
  def __init__(self):
@@ -11,6 +23,8 @@ class GraphHook(BaseHook):
11
23
  except ImportError:
12
24
  raise ImportError("GraphHook requires matplotlib and scipy to be installed")
13
25
 
26
+ # https://github.com/python/cpython/issues/125235#issuecomment-2412948604
27
+
14
28
  def query_graph_built(
15
29
  self,
16
30
  graph: nx.DiGraph,
trilogy/parsing/common.py CHANGED
@@ -1,3 +1,4 @@
1
+ from datetime import date, datetime
1
2
  from typing import List, Tuple
2
3
 
3
4
  from trilogy.constants import (
@@ -37,14 +38,22 @@ def process_function_args(
37
38
  args,
38
39
  meta: Meta | None,
39
40
  environment: Environment,
40
- ):
41
- final: List[Concept | Function] = []
41
+ ) -> List[Concept | Function | str | int | float | date | datetime]:
42
+ final: List[Concept | Function | str | int | float | date | datetime] = []
42
43
  for arg in args:
43
44
  # if a function has an anonymous function argument
44
45
  # create an implicit concept
45
- while isinstance(arg, Parenthetical):
46
- arg = arg.content
47
- if isinstance(arg, Function):
46
+ if isinstance(arg, Parenthetical):
47
+ processed = process_function_args([arg.content], meta, environment)
48
+ final.append(
49
+ Function(
50
+ operator=FunctionType.PARENTHETICAL,
51
+ arguments=processed,
52
+ output_datatype=arg_to_datatype(processed[0]),
53
+ output_purpose=function_args_to_output_purpose(processed),
54
+ )
55
+ )
56
+ elif isinstance(arg, Function):
48
57
  # if it's not an aggregate function, we can skip the virtual concepts
49
58
  # to simplify anonymous function handling
50
59
  if (
@@ -1,4 +1,5 @@
1
1
  from dataclasses import dataclass
2
+ from datetime import date, datetime
2
3
  from os.path import dirname, join
3
4
  from pathlib import Path
4
5
  from re import IGNORECASE
@@ -570,6 +571,7 @@ class ParseToObjects(Transformer):
570
571
  for new_concept in output.derived_concepts:
571
572
  if new_concept.metadata:
572
573
  new_concept.metadata.line_number = meta.line
574
+ # output.select.local_concepts[new_concept.address] = new_concept
573
575
  self.environment.add_concept(new_concept)
574
576
 
575
577
  return output
@@ -668,6 +670,12 @@ class ParseToObjects(Transformer):
668
670
  )
669
671
  for column in columns:
670
672
  column.concept = column.concept.with_grain(datasource.grain)
673
+ if datasource.where:
674
+ for x in datasource.where.concept_arguments:
675
+ if x.address not in datasource.output_concepts:
676
+ raise ValueError(
677
+ f"Datasource {name} where condition depends on concept {x.address} that does not exist on the datasource, line {meta.line}."
678
+ )
671
679
  self.environment.add_datasource(datasource, meta=meta)
672
680
  return datasource
673
681
 
@@ -1782,6 +1790,30 @@ class ParseToObjects(Transformer):
1782
1790
  @v_args(meta=True)
1783
1791
  def fcast(self, meta, args) -> Function:
1784
1792
  args = process_function_args(args, meta=meta, environment=self.environment)
1793
+ if isinstance(args[0], str):
1794
+ processed: date | datetime | int | float | bool | str
1795
+ if args[1] == DataType.DATE:
1796
+ processed = date.fromisoformat(args[0])
1797
+ elif args[1] == DataType.DATETIME:
1798
+ processed = datetime.fromisoformat(args[0])
1799
+ elif args[1] == DataType.TIMESTAMP:
1800
+ processed = datetime.fromisoformat(args[0])
1801
+ elif args[1] == DataType.INTEGER:
1802
+ processed = int(args[0])
1803
+ elif args[1] == DataType.FLOAT:
1804
+ processed = float(args[0])
1805
+ elif args[1] == DataType.BOOL:
1806
+ processed = args[0].capitalize() == "True"
1807
+ elif args[1] == DataType.STRING:
1808
+ processed = args[0]
1809
+ else:
1810
+ raise SyntaxError(f"Invalid cast type {args[1]}")
1811
+ return Function(
1812
+ operator=FunctionType.CONSTANT,
1813
+ output_datatype=args[1],
1814
+ output_purpose=Purpose.CONSTANT,
1815
+ arguments=[processed],
1816
+ )
1785
1817
  output_datatype = args[1]
1786
1818
  return Function(
1787
1819
  operator=FunctionType.CAST,
@@ -193,7 +193,9 @@
193
193
  _math_functions: fmul | fdiv | fadd | fsub | fround | fmod | fabs
194
194
 
195
195
  //generic
196
- fcast: "cast"i "(" expr "as"i data_type ")"
196
+ _fcast_primary: "cast"i "(" expr "as"i data_type ")"
197
+ _fcast_alt: expr "::" data_type
198
+ fcast: _fcast_primary | _fcast_alt
197
199
  concat: ("concat"i "(" (expr ",")* expr ")") | (expr "||" expr)
198
200
  fcoalesce: "coalesce"i "(" (expr ",")* expr ")"
199
201
  fcase_when: "WHEN"i conditional "THEN"i expr