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.
- {pytrilogy-0.0.2.48.dist-info → pytrilogy-0.0.2.50.dist-info}/METADATA +1 -1
- {pytrilogy-0.0.2.48.dist-info → pytrilogy-0.0.2.50.dist-info}/RECORD +38 -38
- trilogy/__init__.py +1 -1
- trilogy/core/enums.py +11 -0
- trilogy/core/functions.py +4 -1
- trilogy/core/models.py +29 -14
- trilogy/core/processing/concept_strategies_v3.py +3 -3
- trilogy/core/processing/node_generators/common.py +0 -2
- trilogy/core/processing/node_generators/filter_node.py +0 -3
- trilogy/core/processing/node_generators/group_node.py +0 -1
- trilogy/core/processing/node_generators/group_to_node.py +0 -2
- trilogy/core/processing/node_generators/multiselect_node.py +0 -2
- trilogy/core/processing/node_generators/node_merge_node.py +0 -1
- trilogy/core/processing/node_generators/rowset_node.py +27 -8
- trilogy/core/processing/node_generators/select_merge_node.py +138 -59
- trilogy/core/processing/node_generators/union_node.py +0 -1
- trilogy/core/processing/node_generators/unnest_node.py +0 -2
- trilogy/core/processing/node_generators/window_node.py +0 -2
- trilogy/core/processing/nodes/base_node.py +28 -3
- trilogy/core/processing/nodes/filter_node.py +0 -3
- trilogy/core/processing/nodes/group_node.py +9 -6
- trilogy/core/processing/nodes/merge_node.py +3 -4
- trilogy/core/processing/nodes/select_node_v2.py +5 -4
- trilogy/core/processing/nodes/union_node.py +0 -3
- trilogy/core/processing/nodes/unnest_node.py +0 -3
- trilogy/core/processing/nodes/window_node.py +0 -3
- trilogy/core/processing/utility.py +4 -1
- trilogy/core/query_processor.py +3 -8
- trilogy/dialect/base.py +14 -2
- trilogy/dialect/duckdb.py +7 -0
- trilogy/hooks/graph_hook.py +14 -0
- trilogy/parsing/common.py +14 -5
- trilogy/parsing/parse_engine.py +32 -0
- trilogy/parsing/trilogy.lark +3 -1
- {pytrilogy-0.0.2.48.dist-info → pytrilogy-0.0.2.50.dist-info}/LICENSE.md +0 -0
- {pytrilogy-0.0.2.48.dist-info → pytrilogy-0.0.2.50.dist-info}/WHEEL +0 -0
- {pytrilogy-0.0.2.48.dist-info → pytrilogy-0.0.2.50.dist-info}/entry_points.txt +0 -0
- {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
|
trilogy/core/query_processor.py
CHANGED
|
@@ -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 =
|
|
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
|
|
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,
|
|
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
|
|
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
|
trilogy/hooks/graph_hook.py
CHANGED
|
@@ -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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
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 (
|
trilogy/parsing/parse_engine.py
CHANGED
|
@@ -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,
|
trilogy/parsing/trilogy.lark
CHANGED
|
@@ -193,7 +193,9 @@
|
|
|
193
193
|
_math_functions: fmul | fdiv | fadd | fsub | fround | fmod | fabs
|
|
194
194
|
|
|
195
195
|
//generic
|
|
196
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|