pytrilogy 0.0.2.15__py3-none-any.whl → 0.0.2.18__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.15.dist-info → pytrilogy-0.0.2.18.dist-info}/METADATA +12 -8
- pytrilogy-0.0.2.18.dist-info/RECORD +83 -0
- trilogy/__init__.py +1 -1
- trilogy/constants.py +1 -1
- trilogy/core/enums.py +1 -0
- trilogy/core/functions.py +11 -0
- trilogy/core/models.py +105 -59
- trilogy/core/optimization.py +15 -9
- trilogy/core/processing/concept_strategies_v3.py +372 -145
- trilogy/core/processing/node_generators/basic_node.py +27 -55
- trilogy/core/processing/node_generators/common.py +6 -7
- trilogy/core/processing/node_generators/filter_node.py +28 -31
- trilogy/core/processing/node_generators/group_node.py +14 -2
- trilogy/core/processing/node_generators/group_to_node.py +3 -1
- trilogy/core/processing/node_generators/multiselect_node.py +3 -0
- trilogy/core/processing/node_generators/node_merge_node.py +14 -9
- trilogy/core/processing/node_generators/rowset_node.py +12 -12
- trilogy/core/processing/node_generators/select_merge_node.py +302 -0
- trilogy/core/processing/node_generators/select_node.py +7 -511
- trilogy/core/processing/node_generators/unnest_node.py +4 -3
- trilogy/core/processing/node_generators/window_node.py +12 -37
- trilogy/core/processing/nodes/__init__.py +0 -2
- trilogy/core/processing/nodes/base_node.py +69 -20
- trilogy/core/processing/nodes/filter_node.py +3 -0
- trilogy/core/processing/nodes/group_node.py +18 -17
- trilogy/core/processing/nodes/merge_node.py +4 -10
- trilogy/core/processing/nodes/select_node_v2.py +28 -14
- trilogy/core/processing/nodes/window_node.py +1 -2
- trilogy/core/processing/utility.py +51 -3
- trilogy/core/query_processor.py +17 -73
- trilogy/dialect/base.py +8 -3
- trilogy/dialect/common.py +65 -10
- trilogy/dialect/duckdb.py +4 -1
- trilogy/dialect/sql_server.py +3 -3
- trilogy/executor.py +5 -0
- trilogy/hooks/query_debugger.py +5 -3
- trilogy/parsing/parse_engine.py +67 -39
- trilogy/parsing/render.py +2 -0
- trilogy/parsing/trilogy.lark +6 -3
- pytrilogy-0.0.2.15.dist-info/RECORD +0 -82
- {pytrilogy-0.0.2.15.dist-info → pytrilogy-0.0.2.18.dist-info}/LICENSE.md +0 -0
- {pytrilogy-0.0.2.15.dist-info → pytrilogy-0.0.2.18.dist-info}/WHEEL +0 -0
- {pytrilogy-0.0.2.15.dist-info → pytrilogy-0.0.2.18.dist-info}/entry_points.txt +0 -0
- {pytrilogy-0.0.2.15.dist-info → pytrilogy-0.0.2.18.dist-info}/top_level.txt +0 -0
trilogy/core/query_processor.py
CHANGED
|
@@ -4,9 +4,9 @@ from trilogy.core.env_processor import generate_graph
|
|
|
4
4
|
from trilogy.core.graph_models import ReferenceGraph
|
|
5
5
|
from trilogy.core.constants import CONSTANT_DATASET
|
|
6
6
|
from trilogy.core.processing.concept_strategies_v3 import source_query_concepts
|
|
7
|
-
from trilogy.core.enums import
|
|
7
|
+
from trilogy.core.enums import BooleanOperator
|
|
8
8
|
from trilogy.constants import CONFIG, DEFAULT_NAMESPACE
|
|
9
|
-
from trilogy.core.processing.nodes import
|
|
9
|
+
from trilogy.core.processing.nodes import SelectNode, StrategyNode, History
|
|
10
10
|
from trilogy.core.models import (
|
|
11
11
|
Concept,
|
|
12
12
|
Environment,
|
|
@@ -193,8 +193,7 @@ def resolve_cte_base_name_and_alias_v2(
|
|
|
193
193
|
) -> Tuple[str | None, str | None]:
|
|
194
194
|
joins: List[Join] = [join for join in raw_joins if isinstance(join, Join)]
|
|
195
195
|
if (
|
|
196
|
-
|
|
197
|
-
and isinstance(source.datasources[0], Datasource)
|
|
196
|
+
isinstance(source.datasources[0], Datasource)
|
|
198
197
|
and not source.datasources[0].name == CONSTANT_DATASET
|
|
199
198
|
):
|
|
200
199
|
ds = source.datasources[0]
|
|
@@ -265,9 +264,7 @@ def datasource_to_ctes(
|
|
|
265
264
|
existence_map = source_map
|
|
266
265
|
|
|
267
266
|
human_id = generate_cte_name(query_datasource.full_name, name_map)
|
|
268
|
-
|
|
269
|
-
f"Finished building source map for {human_id} with {len(parents)} parents, have {source_map}, query_datasource had non-empty keys {[k for k, v in query_datasource.source_map.items() if v]} and existence had non-empty keys {[k for k, v in query_datasource.existence_source_map.items() if v]} "
|
|
270
|
-
)
|
|
267
|
+
|
|
271
268
|
final_joins = [
|
|
272
269
|
x
|
|
273
270
|
for x in [base_join_to_join(join, parents) for join in query_datasource.joins]
|
|
@@ -317,29 +314,6 @@ def datasource_to_ctes(
|
|
|
317
314
|
return output
|
|
318
315
|
|
|
319
316
|
|
|
320
|
-
def append_existence_check(
|
|
321
|
-
node: StrategyNode,
|
|
322
|
-
environment: Environment,
|
|
323
|
-
graph: ReferenceGraph,
|
|
324
|
-
history: History | None = None,
|
|
325
|
-
):
|
|
326
|
-
# we if we have a where clause doing an existence check
|
|
327
|
-
# treat that as separate subquery
|
|
328
|
-
if (where := node.conditions) and where.existence_arguments:
|
|
329
|
-
for subselect in where.existence_arguments:
|
|
330
|
-
if not subselect:
|
|
331
|
-
continue
|
|
332
|
-
|
|
333
|
-
eds = source_query_concepts(
|
|
334
|
-
[*subselect], environment=environment, g=graph, history=history
|
|
335
|
-
)
|
|
336
|
-
logger.info(
|
|
337
|
-
f"{LOGGER_PREFIX} fetching existence clause inputs {[str(c) for c in subselect]}"
|
|
338
|
-
)
|
|
339
|
-
node.add_parents([eds])
|
|
340
|
-
node.add_existence_concepts([*subselect])
|
|
341
|
-
|
|
342
|
-
|
|
343
317
|
def get_query_node(
|
|
344
318
|
environment: Environment,
|
|
345
319
|
statement: SelectStatement | MultiSelectStatement,
|
|
@@ -354,16 +328,6 @@ def get_query_node(
|
|
|
354
328
|
raise ValueError(f"Statement has no output components {statement}")
|
|
355
329
|
|
|
356
330
|
search_concepts: list[Concept] = statement.output_components
|
|
357
|
-
nest_where = statement.where_clause_category == SelectFiltering.IMPLICIT
|
|
358
|
-
|
|
359
|
-
# if all are aggregates, we've pushed the filtering inside the aggregates anyway
|
|
360
|
-
all_aggregate = all([x.is_aggregate for x in search_concepts])
|
|
361
|
-
|
|
362
|
-
if nest_where and statement.where_clause and not all_aggregate:
|
|
363
|
-
search_concepts = unique(
|
|
364
|
-
statement.where_clause.row_arguments + search_concepts, "address"
|
|
365
|
-
)
|
|
366
|
-
nest_where = True
|
|
367
331
|
|
|
368
332
|
ods: StrategyNode = source_query_concepts(
|
|
369
333
|
search_concepts,
|
|
@@ -376,45 +340,24 @@ def get_query_node(
|
|
|
376
340
|
raise ValueError(
|
|
377
341
|
f"Could not find source query concepts for {[x.address for x in search_concepts]}"
|
|
378
342
|
)
|
|
379
|
-
ds: StrategyNode
|
|
380
|
-
if nest_where and statement.where_clause:
|
|
381
|
-
if not all_aggregate:
|
|
382
|
-
ods.conditions = statement.where_clause.conditional
|
|
383
|
-
ods.set_output_concepts(statement.output_components)
|
|
384
|
-
append_existence_check(ods, environment, graph, history)
|
|
385
|
-
ds = GroupNode(
|
|
386
|
-
output_concepts=statement.output_components,
|
|
387
|
-
input_concepts=statement.output_components,
|
|
388
|
-
parents=[ods],
|
|
389
|
-
environment=ods.environment,
|
|
390
|
-
g=ods.g,
|
|
391
|
-
partial_concepts=ods.partial_concepts,
|
|
392
|
-
)
|
|
393
|
-
# we can still check existence here.
|
|
394
|
-
|
|
395
|
-
elif statement.where_clause:
|
|
396
|
-
ds = SelectNode(
|
|
397
|
-
output_concepts=statement.output_components,
|
|
398
|
-
input_concepts=ods.input_concepts,
|
|
399
|
-
parents=[ods],
|
|
400
|
-
environment=ods.environment,
|
|
401
|
-
g=ods.g,
|
|
402
|
-
partial_concepts=ods.partial_concepts,
|
|
403
|
-
conditions=statement.where_clause.conditional,
|
|
404
|
-
)
|
|
405
|
-
append_existence_check(ds, environment, graph)
|
|
406
|
-
|
|
407
|
-
else:
|
|
408
|
-
ds = ods
|
|
343
|
+
ds: StrategyNode = ods
|
|
409
344
|
if statement.having_clause:
|
|
345
|
+
final = statement.having_clause.conditional
|
|
410
346
|
if ds.conditions:
|
|
411
|
-
|
|
347
|
+
final = Conditional(
|
|
412
348
|
left=ds.conditions,
|
|
413
349
|
right=statement.having_clause.conditional,
|
|
414
350
|
operator=BooleanOperator.AND,
|
|
415
351
|
)
|
|
416
|
-
|
|
417
|
-
|
|
352
|
+
ds = SelectNode(
|
|
353
|
+
output_concepts=statement.output_components,
|
|
354
|
+
input_concepts=ds.output_concepts,
|
|
355
|
+
parents=[ds],
|
|
356
|
+
environment=ds.environment,
|
|
357
|
+
g=ds.g,
|
|
358
|
+
partial_concepts=ds.partial_concepts,
|
|
359
|
+
conditions=final,
|
|
360
|
+
)
|
|
418
361
|
return ds
|
|
419
362
|
|
|
420
363
|
|
|
@@ -477,6 +420,7 @@ def process_query(
|
|
|
477
420
|
hooks: List[BaseHook] | None = None,
|
|
478
421
|
) -> ProcessedQuery:
|
|
479
422
|
hooks = hooks or []
|
|
423
|
+
statement.refresh_bindings(environment)
|
|
480
424
|
graph = generate_graph(environment)
|
|
481
425
|
root_datasource = get_query_datasources(
|
|
482
426
|
environment=environment, graph=graph, statement=statement, hooks=hooks
|
trilogy/dialect/base.py
CHANGED
|
@@ -121,6 +121,7 @@ FUNCTION_MAP = {
|
|
|
121
121
|
FunctionType.CASE: lambda x: render_case(x),
|
|
122
122
|
FunctionType.SPLIT: lambda x: f"split({x[0]}, {x[1]})",
|
|
123
123
|
FunctionType.IS_NULL: lambda x: f"isnull({x[0]})",
|
|
124
|
+
FunctionType.BOOL: lambda x: f"CASE WHEN {x[0]} THEN TRUE ELSE FALSE END",
|
|
124
125
|
# complex
|
|
125
126
|
FunctionType.INDEX_ACCESS: lambda x: f"{x[0]}[{x[1]}]",
|
|
126
127
|
FunctionType.MAP_ACCESS: lambda x: f"{x[0]}[{x[1]}][1]",
|
|
@@ -176,8 +177,8 @@ FUNCTION_MAP = {
|
|
|
176
177
|
|
|
177
178
|
FUNCTION_GRAIN_MATCH_MAP = {
|
|
178
179
|
**FUNCTION_MAP,
|
|
179
|
-
FunctionType.COUNT_DISTINCT: lambda args: f"{args[0]}",
|
|
180
|
-
FunctionType.COUNT: lambda args: f"{args[0]}",
|
|
180
|
+
FunctionType.COUNT_DISTINCT: lambda args: f"CASE WHEN{args[0]} IS NOT NULL THEN 1 ELSE 0 END",
|
|
181
|
+
FunctionType.COUNT: lambda args: f"CASE WHEN {args[0]} IS NOT NULL THEN 1 ELSE 0 END",
|
|
181
182
|
FunctionType.SUM: lambda args: f"{args[0]}",
|
|
182
183
|
FunctionType.AVG: lambda args: f"{args[0]}",
|
|
183
184
|
FunctionType.MAX: lambda args: f"{args[0]}",
|
|
@@ -582,8 +583,11 @@ class BaseDialect:
|
|
|
582
583
|
having: Conditional | Parenthetical | Comparison | None = None
|
|
583
584
|
materialized = {x for x, v in cte.source_map.items() if v}
|
|
584
585
|
if cte.condition:
|
|
585
|
-
if
|
|
586
|
+
if not cte.group_to_grain or is_scalar_condition(
|
|
587
|
+
cte.condition, materialized=materialized
|
|
588
|
+
):
|
|
586
589
|
where = cte.condition
|
|
590
|
+
|
|
587
591
|
else:
|
|
588
592
|
components = decompose_condition(cte.condition)
|
|
589
593
|
for x in components:
|
|
@@ -608,6 +612,7 @@ class BaseDialect:
|
|
|
608
612
|
join,
|
|
609
613
|
self.QUOTE_CHARACTER,
|
|
610
614
|
self.render_concept_sql,
|
|
615
|
+
self.render_expr,
|
|
611
616
|
cte,
|
|
612
617
|
self.UNNEST_MODE,
|
|
613
618
|
)
|
trilogy/dialect/common.py
CHANGED
|
@@ -1,6 +1,13 @@
|
|
|
1
|
-
from trilogy.core.models import
|
|
1
|
+
from trilogy.core.models import (
|
|
2
|
+
Join,
|
|
3
|
+
InstantiatedUnnestJoin,
|
|
4
|
+
CTE,
|
|
5
|
+
Concept,
|
|
6
|
+
Function,
|
|
7
|
+
RawColumnExpr,
|
|
8
|
+
)
|
|
2
9
|
from trilogy.core.enums import UnnestMode, Modifier
|
|
3
|
-
from typing import
|
|
10
|
+
from typing import Callable
|
|
4
11
|
|
|
5
12
|
|
|
6
13
|
def null_wrapper(lval: str, rval: str, modifiers: list[Modifier]) -> str:
|
|
@@ -21,19 +28,39 @@ def render_unnest(
|
|
|
21
28
|
return f"{render_func(concept, cte, False)} as unnest_wrapper ({quote_character}{concept.safe_address}{quote_character})"
|
|
22
29
|
|
|
23
30
|
|
|
31
|
+
def render_join_concept(
|
|
32
|
+
name: str,
|
|
33
|
+
quote_character: str,
|
|
34
|
+
cte: CTE,
|
|
35
|
+
concept: Concept,
|
|
36
|
+
render_expr,
|
|
37
|
+
inlined_ctes: set[str],
|
|
38
|
+
):
|
|
39
|
+
if cte.name in inlined_ctes:
|
|
40
|
+
ds = cte.source.datasources[0]
|
|
41
|
+
raw_content = ds.get_alias(concept)
|
|
42
|
+
if isinstance(raw_content, RawColumnExpr):
|
|
43
|
+
rval = raw_content.text
|
|
44
|
+
return rval
|
|
45
|
+
elif isinstance(raw_content, Function):
|
|
46
|
+
rval = render_expr(raw_content, cte=cte)
|
|
47
|
+
return rval
|
|
48
|
+
return f"{name}.{quote_character}{raw_content}{quote_character}"
|
|
49
|
+
return f"{name}.{quote_character}{concept.safe_address}{quote_character}"
|
|
50
|
+
|
|
51
|
+
|
|
24
52
|
def render_join(
|
|
25
53
|
join: Join | InstantiatedUnnestJoin,
|
|
26
54
|
quote_character: str,
|
|
27
|
-
render_func:
|
|
28
|
-
|
|
55
|
+
render_func: Callable[[Concept, CTE, bool], str],
|
|
56
|
+
render_expr_func: Callable[[Concept, CTE], str],
|
|
57
|
+
cte: CTE,
|
|
29
58
|
unnest_mode: UnnestMode = UnnestMode.CROSS_APPLY,
|
|
30
59
|
) -> str | None:
|
|
31
60
|
# {% for key in join.joinkeys %}{{ key.inner }} = {{ key.outer}}{% endfor %}
|
|
32
61
|
if isinstance(join, InstantiatedUnnestJoin):
|
|
33
62
|
if unnest_mode == UnnestMode.DIRECT:
|
|
34
63
|
return None
|
|
35
|
-
if not render_func:
|
|
36
|
-
raise ValueError("must provide a render function to build an unnest joins")
|
|
37
64
|
if not cte:
|
|
38
65
|
raise ValueError("must provide a cte to build an unnest joins")
|
|
39
66
|
if unnest_mode == UnnestMode.CROSS_JOIN:
|
|
@@ -46,8 +73,22 @@ def render_join(
|
|
|
46
73
|
right_base = join.right_ref
|
|
47
74
|
base_joinkeys = [
|
|
48
75
|
null_wrapper(
|
|
49
|
-
|
|
50
|
-
|
|
76
|
+
render_join_concept(
|
|
77
|
+
left_name,
|
|
78
|
+
quote_character,
|
|
79
|
+
join.left_cte,
|
|
80
|
+
key.concept,
|
|
81
|
+
render_expr_func,
|
|
82
|
+
join.inlined_ctes,
|
|
83
|
+
),
|
|
84
|
+
render_join_concept(
|
|
85
|
+
right_name,
|
|
86
|
+
quote_character,
|
|
87
|
+
join.right_cte,
|
|
88
|
+
key.concept,
|
|
89
|
+
render_expr_func,
|
|
90
|
+
join.inlined_ctes,
|
|
91
|
+
),
|
|
51
92
|
modifiers=key.concept.modifiers or [],
|
|
52
93
|
)
|
|
53
94
|
for key in join.joinkeys
|
|
@@ -56,8 +97,22 @@ def render_join(
|
|
|
56
97
|
base_joinkeys.extend(
|
|
57
98
|
[
|
|
58
99
|
null_wrapper(
|
|
59
|
-
|
|
60
|
-
|
|
100
|
+
render_join_concept(
|
|
101
|
+
left_name,
|
|
102
|
+
quote_character,
|
|
103
|
+
join.left_cte,
|
|
104
|
+
pair.left,
|
|
105
|
+
render_expr_func,
|
|
106
|
+
join.inlined_ctes,
|
|
107
|
+
),
|
|
108
|
+
render_join_concept(
|
|
109
|
+
right_name,
|
|
110
|
+
quote_character,
|
|
111
|
+
join.right_cte,
|
|
112
|
+
pair.right,
|
|
113
|
+
render_expr_func,
|
|
114
|
+
join.inlined_ctes,
|
|
115
|
+
),
|
|
61
116
|
modifiers=pair.modifiers
|
|
62
117
|
+ (pair.left.modifiers or [])
|
|
63
118
|
+ (pair.right.modifiers or []),
|
trilogy/dialect/duckdb.py
CHANGED
|
@@ -36,9 +36,12 @@ FUNCTION_MAP = {
|
|
|
36
36
|
# we may return a static value
|
|
37
37
|
FUNCTION_GRAIN_MATCH_MAP = {
|
|
38
38
|
**FUNCTION_MAP,
|
|
39
|
-
FunctionType.
|
|
39
|
+
FunctionType.COUNT_DISTINCT: lambda args: f"CASE WHEN{args[0]} IS NOT NULL THEN 1 ELSE 0 END",
|
|
40
|
+
FunctionType.COUNT: lambda args: f"CASE WHEN {args[0]} IS NOT NULL THEN 1 ELSE 0 END",
|
|
40
41
|
FunctionType.SUM: lambda args: f"{args[0]}",
|
|
41
42
|
FunctionType.AVG: lambda args: f"{args[0]}",
|
|
43
|
+
FunctionType.MAX: lambda args: f"{args[0]}",
|
|
44
|
+
FunctionType.MIN: lambda args: f"{args[0]}",
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
DUCKDB_TEMPLATE = Template(
|
trilogy/dialect/sql_server.py
CHANGED
|
@@ -41,9 +41,9 @@ TSQL_TEMPLATE = Template(
|
|
|
41
41
|
"""{%- if ctes %}
|
|
42
42
|
WITH {% for cte in ctes %}
|
|
43
43
|
{{cte.name}} as ({{cte.statement}}){% if not loop.last %},{% endif %}{% endfor %}{% endif %}
|
|
44
|
-
{%- if full_select -%}
|
|
45
|
-
{{
|
|
46
|
-
{
|
|
44
|
+
{%- if full_select -%}{{full_select}}
|
|
45
|
+
{%- else -%}{%- if comment %}
|
|
46
|
+
-- {{ comment }}{% endif %}
|
|
47
47
|
SELECT
|
|
48
48
|
{%- if limit is not none %}
|
|
49
49
|
TOP {{ limit }}{% endif %}
|
trilogy/executor.py
CHANGED
|
@@ -129,6 +129,11 @@ class Executor(object):
|
|
|
129
129
|
["name"],
|
|
130
130
|
)
|
|
131
131
|
|
|
132
|
+
@execute_query.register
|
|
133
|
+
def _(self, query: str) -> CursorResult:
|
|
134
|
+
|
|
135
|
+
return self.execute_text(query)[-1]
|
|
136
|
+
|
|
132
137
|
@execute_query.register
|
|
133
138
|
def _(self, query: SelectStatement) -> CursorResult:
|
|
134
139
|
sql = self.generator.generate_queries(
|
trilogy/hooks/query_debugger.py
CHANGED
|
@@ -31,6 +31,9 @@ def print_recursive_resolved(
|
|
|
31
31
|
extra.append("filter")
|
|
32
32
|
if input.group_required:
|
|
33
33
|
extra.append("group")
|
|
34
|
+
output = [c.address for c in input.output_concepts[:3]]
|
|
35
|
+
if len(input.output_concepts) > 3:
|
|
36
|
+
output.append("...")
|
|
34
37
|
display = [
|
|
35
38
|
(
|
|
36
39
|
" " * depth,
|
|
@@ -40,7 +43,7 @@ def print_recursive_resolved(
|
|
|
40
43
|
">",
|
|
41
44
|
# [c.address for c in input.input_concepts],
|
|
42
45
|
"->",
|
|
43
|
-
|
|
46
|
+
output,
|
|
44
47
|
)
|
|
45
48
|
]
|
|
46
49
|
if isinstance(input, QueryDatasource):
|
|
@@ -86,7 +89,6 @@ def print_recursive_ctes(input: CTE, depth: int = 0, max_depth: int | None = Non
|
|
|
86
89
|
sql = renderer.render_cte(input).statement
|
|
87
90
|
for line in sql.split("\n"):
|
|
88
91
|
logger.debug(" " * (depth) + line)
|
|
89
|
-
print("-----")
|
|
90
92
|
if isinstance(input, CTE):
|
|
91
93
|
for child in input.parent_ctes:
|
|
92
94
|
print_recursive_ctes(child, depth + 1)
|
|
@@ -130,5 +132,5 @@ class DebuggingHook(BaseHook):
|
|
|
130
132
|
if self.process_nodes != PrintMode.OFF:
|
|
131
133
|
printed = print_recursive_nodes(node, mode=self.process_nodes)
|
|
132
134
|
for row in printed:
|
|
133
|
-
logger.info("".join([str(v) for v in row]))
|
|
135
|
+
# logger.info("".join([str(v) for v in row]))
|
|
134
136
|
print("".join([str(v) for v in row]))
|
trilogy/parsing/parse_engine.py
CHANGED
|
@@ -30,7 +30,7 @@ from trilogy.core.enums import (
|
|
|
30
30
|
WindowType,
|
|
31
31
|
DatePart,
|
|
32
32
|
ShowCategory,
|
|
33
|
-
|
|
33
|
+
FunctionClass,
|
|
34
34
|
)
|
|
35
35
|
from trilogy.core.exceptions import InvalidSyntaxException, UndefinedConceptException
|
|
36
36
|
from trilogy.core.functions import (
|
|
@@ -50,6 +50,7 @@ from trilogy.core.functions import (
|
|
|
50
50
|
CurrentDate,
|
|
51
51
|
CurrentDatetime,
|
|
52
52
|
IsNull,
|
|
53
|
+
Bool,
|
|
53
54
|
SubString,
|
|
54
55
|
StrPos,
|
|
55
56
|
)
|
|
@@ -600,7 +601,7 @@ class ParseToObjects(Transformer):
|
|
|
600
601
|
return args[3:-3]
|
|
601
602
|
|
|
602
603
|
def raw_column_assignment(self, args):
|
|
603
|
-
return RawColumnExpr(text=args[
|
|
604
|
+
return RawColumnExpr(text=args[1])
|
|
604
605
|
|
|
605
606
|
@v_args(meta=True)
|
|
606
607
|
def datasource(self, meta: Meta, args):
|
|
@@ -840,6 +841,7 @@ class ParseToObjects(Transformer):
|
|
|
840
841
|
grain: Grain | None = args[3]
|
|
841
842
|
else:
|
|
842
843
|
grain = None
|
|
844
|
+
|
|
843
845
|
new_datasource = select.to_datasource(
|
|
844
846
|
namespace=(
|
|
845
847
|
self.environment.namespace
|
|
@@ -930,7 +932,8 @@ class ParseToObjects(Transformer):
|
|
|
930
932
|
order_by=order_by,
|
|
931
933
|
meta=Metadata(line_number=meta.line),
|
|
932
934
|
)
|
|
933
|
-
|
|
935
|
+
locally_derived: set[str] = set()
|
|
936
|
+
all_in_output: set[str] = set()
|
|
934
937
|
for item in select_items:
|
|
935
938
|
# we don't know the grain of an aggregate at assignment time
|
|
936
939
|
# so rebuild at this point in the tree
|
|
@@ -938,59 +941,79 @@ class ParseToObjects(Transformer):
|
|
|
938
941
|
if isinstance(item.content, ConceptTransform):
|
|
939
942
|
new_concept = item.content.output.with_select_context(
|
|
940
943
|
output.grain,
|
|
941
|
-
conditional=
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
944
|
+
conditional=None,
|
|
945
|
+
# conditional=(
|
|
946
|
+
# output.where_clause.conditional
|
|
947
|
+
# if output.where_clause
|
|
948
|
+
# and output.where_clause_category == SelectFiltering.IMPLICIT
|
|
949
|
+
# else None
|
|
950
|
+
# ),
|
|
947
951
|
environment=self.environment,
|
|
948
952
|
)
|
|
949
953
|
self.environment.add_concept(new_concept, meta=meta)
|
|
950
954
|
item.content.output = new_concept
|
|
955
|
+
locally_derived.add(new_concept.address)
|
|
956
|
+
all_in_output.add(new_concept.address)
|
|
951
957
|
elif isinstance(item.content, Concept):
|
|
952
958
|
# Sometimes cached values here don't have the latest info
|
|
953
|
-
#
|
|
959
|
+
# but we can't just use environment, as it might not have the right grain.
|
|
954
960
|
item.content = self.environment.concepts[
|
|
955
961
|
item.content.address
|
|
956
962
|
].with_grain(item.content.grain)
|
|
957
|
-
|
|
958
|
-
# else:
|
|
959
|
-
# item.content = (
|
|
960
|
-
# item.content.with_filter(
|
|
961
|
-
# output.where_clause.conditional, environment=self.environment
|
|
962
|
-
# )
|
|
963
|
-
# if output.where_clause
|
|
964
|
-
# and output.where_clause_category == SelectFiltering.IMPLICIT
|
|
965
|
-
# else item.content
|
|
966
|
-
# )
|
|
967
|
-
|
|
963
|
+
all_in_output.add(item.content.address)
|
|
968
964
|
if order_by:
|
|
969
965
|
for orderitem in order_by.items:
|
|
970
966
|
if isinstance(orderitem.expr, Concept):
|
|
971
967
|
if orderitem.expr.purpose == Purpose.METRIC:
|
|
972
968
|
orderitem.expr = orderitem.expr.with_select_context(
|
|
973
969
|
output.grain,
|
|
974
|
-
conditional=
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
970
|
+
conditional=None,
|
|
971
|
+
# conditional=(
|
|
972
|
+
# output.where_clause.conditional
|
|
973
|
+
# if output.where_clause
|
|
974
|
+
# and output.where_clause_category
|
|
975
|
+
# == SelectFiltering.IMPLICIT
|
|
976
|
+
# else None
|
|
977
|
+
# ),
|
|
981
978
|
environment=self.environment,
|
|
982
979
|
)
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
980
|
+
if output.where_clause:
|
|
981
|
+
for concept in output.where_clause.concept_arguments:
|
|
982
|
+
|
|
983
|
+
if (
|
|
984
|
+
concept.lineage
|
|
985
|
+
and isinstance(concept.lineage, Function)
|
|
986
|
+
and concept.lineage.operator
|
|
987
|
+
in FunctionClass.AGGREGATE_FUNCTIONS.value
|
|
988
|
+
):
|
|
989
|
+
if concept.address in locally_derived:
|
|
990
|
+
raise SyntaxError(
|
|
991
|
+
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}"
|
|
992
|
+
)
|
|
993
|
+
|
|
994
|
+
if (
|
|
995
|
+
concept.lineage
|
|
996
|
+
and isinstance(concept.lineage, AggregateWrapper)
|
|
997
|
+
and concept.lineage.function.operator
|
|
998
|
+
in FunctionClass.AGGREGATE_FUNCTIONS.value
|
|
999
|
+
):
|
|
1000
|
+
if concept.address in locally_derived:
|
|
1001
|
+
raise SyntaxError(
|
|
1002
|
+
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}"
|
|
1003
|
+
)
|
|
1004
|
+
if output.having_clause:
|
|
1005
|
+
for concept in output.having_clause.concept_arguments:
|
|
1006
|
+
if concept.address not in all_in_output:
|
|
1007
|
+
raise SyntaxError(
|
|
1008
|
+
f"Cannot reference a column ({concept.address}) that is not in the select projection in the HAVING clause, move to WHERE; Line: {meta.line}"
|
|
1009
|
+
)
|
|
1010
|
+
if output.order_by:
|
|
1011
|
+
for concept in output.order_by.concept_arguments:
|
|
1012
|
+
if concept.address not in all_in_output:
|
|
1013
|
+
raise SyntaxError(
|
|
1014
|
+
f"Cannot order by a column that is not in the output projection; {meta.line}"
|
|
1015
|
+
)
|
|
1016
|
+
|
|
994
1017
|
return output
|
|
995
1018
|
|
|
996
1019
|
@v_args(meta=True)
|
|
@@ -1832,6 +1855,11 @@ class ParseToObjects(Transformer):
|
|
|
1832
1855
|
args = process_function_args(args, meta=meta, environment=self.environment)
|
|
1833
1856
|
return IsNull(args)
|
|
1834
1857
|
|
|
1858
|
+
@v_args(meta=True)
|
|
1859
|
+
def fbool(self, meta, args):
|
|
1860
|
+
args = process_function_args(args, meta=meta, environment=self.environment)
|
|
1861
|
+
return Bool(args)
|
|
1862
|
+
|
|
1835
1863
|
|
|
1836
1864
|
def unpack_visit_error(e: VisitError):
|
|
1837
1865
|
"""This is required to get exceptions from imports, which would
|
trilogy/parsing/render.py
CHANGED
|
@@ -328,6 +328,8 @@ class Renderer:
|
|
|
328
328
|
inputs = ",".join(self.to_string(c) for c in arg.arguments)
|
|
329
329
|
if arg.operator == FunctionType.CONSTANT:
|
|
330
330
|
return f"{inputs}"
|
|
331
|
+
if arg.operator == FunctionType.INDEX_ACCESS:
|
|
332
|
+
return f"{self.to_string(arg.arguments[0])}[{self.to_string(arg.arguments[1])}]"
|
|
331
333
|
return f"{arg.operator.value}({inputs})"
|
|
332
334
|
|
|
333
335
|
@to_string.register
|
trilogy/parsing/trilogy.lark
CHANGED
|
@@ -47,9 +47,11 @@
|
|
|
47
47
|
|
|
48
48
|
//column_assignment
|
|
49
49
|
//figure out if we want static
|
|
50
|
-
column_assignment: (
|
|
50
|
+
column_assignment: (raw_column_assignment | IDENTIFIER | _static_functions ) ":" concept_assignment
|
|
51
|
+
|
|
52
|
+
RAW_ENTRY.1: /raw\s*\(/s
|
|
51
53
|
|
|
52
|
-
raw_column_assignment:
|
|
54
|
+
raw_column_assignment: RAW_ENTRY MULTILINE_STRING ")"
|
|
53
55
|
|
|
54
56
|
column_assignment_list : column_assignment ("," column_assignment)* ","?
|
|
55
57
|
|
|
@@ -185,8 +187,9 @@
|
|
|
185
187
|
fcase: "CASE"i (fcase_when)* (fcase_else)? "END"i
|
|
186
188
|
len: "len"i "(" expr ")"
|
|
187
189
|
fnot: "NOT"i expr
|
|
190
|
+
fbool: "bool"i "(" expr ")"
|
|
188
191
|
|
|
189
|
-
_generic_functions: fcast | concat | fcoalesce | fcase | len | fnot
|
|
192
|
+
_generic_functions: fcast | concat | fcoalesce | fcase | len | fnot | fbool
|
|
190
193
|
|
|
191
194
|
//constant
|
|
192
195
|
CURRENT_DATE.1: /current_date\(\)/
|
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
trilogy/__init__.py,sha256=blYi5mQGhWKBA8TAqC2T8y01GnT4mPEI1MaT2P3gwAI,291
|
|
2
|
-
trilogy/compiler.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
-
trilogy/constants.py,sha256=Ijos7_TEajKhZ7OJ_TreEYFddW1V33AVymDDrxP-ZHk,1234
|
|
4
|
-
trilogy/engine.py,sha256=R5ubIxYyrxRExz07aZCUfrTsoXCHQ8DKFTDsobXdWdA,1102
|
|
5
|
-
trilogy/executor.py,sha256=PZr7IF8wS1Oi2WJGE-B3lp70Y8ue2uuauODw02chjdQ,11175
|
|
6
|
-
trilogy/parser.py,sha256=UtuqSiGiCjpMAYgo1bvNq-b7NSzCA5hzbUW31RXaMII,281
|
|
7
|
-
trilogy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
-
trilogy/utility.py,sha256=zM__8r29EsyDW7K9VOHz8yvZC2bXFzh7xKy3cL7GKsk,707
|
|
9
|
-
trilogy/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
10
|
-
trilogy/core/constants.py,sha256=LL8NLvxb3HRnAjvofyLRXqQJijLcYiXAQYQzGarVD-g,128
|
|
11
|
-
trilogy/core/enums.py,sha256=BRYqy-NgIacCYTJo0B11m5XQWSHq5pfxhoLd8pzA3ho,6025
|
|
12
|
-
trilogy/core/env_processor.py,sha256=l7TAB0LalxjTYJdTlcmFIkLXuyxa9lrenWLeZfa9qw0,2276
|
|
13
|
-
trilogy/core/environment_helpers.py,sha256=1miP4is4FEoci01KSAy2VZVYmlmT5TOCOALBekd2muQ,7211
|
|
14
|
-
trilogy/core/ergonomics.py,sha256=w3gwXdgrxNHCuaRdyKg73t6F36tj-wIjQf47WZkHmJk,1465
|
|
15
|
-
trilogy/core/exceptions.py,sha256=NvV_4qLOgKXbpotgRf7c8BANDEvHxlqRPaA53IThQ2o,561
|
|
16
|
-
trilogy/core/functions.py,sha256=ARJAyBjeS415-54k3G_bx807rkPZonEulMaLRxSP7vU,10371
|
|
17
|
-
trilogy/core/graph_models.py,sha256=oJUMSpmYhqXlavckHLpR07GJxuQ8dZ1VbB1fB0KaS8c,2036
|
|
18
|
-
trilogy/core/internal.py,sha256=jNGFHKENnbMiMCtAgsnLZYVSENDK4b5ALecXFZpTDzQ,1075
|
|
19
|
-
trilogy/core/models.py,sha256=bWCklm8A9I0vx8fXWoN0jKJjLkXuyMUUQetT_zbhyRc,149031
|
|
20
|
-
trilogy/core/optimization.py,sha256=7E-Ol51u6ZAxF56F_bzLxgRO-Hu6Yl1ZbPopZJB2tqk,7533
|
|
21
|
-
trilogy/core/query_processor.py,sha256=qMVkaK1Lvr9jEftJwAidMdkb_tRx12G07qynEyl91C8,18801
|
|
22
|
-
trilogy/core/optimizations/__init__.py,sha256=bWQecbeiwiDx9LJnLsa7dkWxdbl2wcnkcTN69JyP8iI,356
|
|
23
|
-
trilogy/core/optimizations/base_optimization.py,sha256=tWWT-xnTbnEU-mNi_isMNbywm8B9WTRsNFwGpeh3rqE,468
|
|
24
|
-
trilogy/core/optimizations/inline_constant.py,sha256=kHNyc2UoaPVdYfVAPAFwnWuk4sJ_IF5faRtVcDOrBtw,1110
|
|
25
|
-
trilogy/core/optimizations/inline_datasource.py,sha256=AATzQ6YrtW_1-aQFjQyTYqEYKBoMFhek7ADfBr4uUdQ,3634
|
|
26
|
-
trilogy/core/optimizations/predicate_pushdown.py,sha256=1l9WnFOSv79e341typG3tTdk0XGl1J_ToQih3LYoGIY,8435
|
|
27
|
-
trilogy/core/processing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
28
|
-
trilogy/core/processing/concept_strategies_v3.py,sha256=ae6FmwiKNiEbOU2GhnzggFMh82MhqxBj9bgr0ituT2w,25633
|
|
29
|
-
trilogy/core/processing/graph_utils.py,sha256=aq-kqk4Iado2HywDxWEejWc-7PGO6Oa-ZQLAM6XWPHw,1199
|
|
30
|
-
trilogy/core/processing/utility.py,sha256=jFLZmzxHq94q29FInr8XjS5YiqJOPPBYqh8Tlgs432Y,17722
|
|
31
|
-
trilogy/core/processing/node_generators/__init__.py,sha256=-mzYkRsaRNa_dfTckYkKVFSR8h8a3ihEiPJDU_tAmDo,672
|
|
32
|
-
trilogy/core/processing/node_generators/basic_node.py,sha256=IHj5jEloUe5yojGRLAzt35FcfHqGviWQdS8ETyvr39Q,3292
|
|
33
|
-
trilogy/core/processing/node_generators/common.py,sha256=3_Ivrq_wersDZ5pnvyHvsAUc07mRggxRGTiDq47O0Rk,8840
|
|
34
|
-
trilogy/core/processing/node_generators/filter_node.py,sha256=gCiv76Cu4idkZRyGkZG44BO50mTqrxYdUSDcZdpd0i4,7724
|
|
35
|
-
trilogy/core/processing/node_generators/group_node.py,sha256=G7SrU2X5kjgzeqzzpnPscQBTDcFMc4m7TR6n8VHLC_A,3762
|
|
36
|
-
trilogy/core/processing/node_generators/group_to_node.py,sha256=yX0uw6YMxhyWVRMZoMFzEkJe3tB5ByFqrTnuRWVcRh4,2446
|
|
37
|
-
trilogy/core/processing/node_generators/multiselect_node.py,sha256=OUjndYjA8xR6yKr-J7R-JxDeYfO6DxmMNNcJiFJzk7g,6138
|
|
38
|
-
trilogy/core/processing/node_generators/node_merge_node.py,sha256=D_jsnfoLMrQc08_JvT0wEDvjyzJAxBpdcZFyDN-feV0,13192
|
|
39
|
-
trilogy/core/processing/node_generators/rowset_node.py,sha256=tc8jt9bMq_HIdLM24sx_ivc7tTlpucQDEvsC2nkOtrY,4454
|
|
40
|
-
trilogy/core/processing/node_generators/select_node.py,sha256=XSMA4kvFdoXlfCpbciXXkbexXkemwUorcAU6P3EwuZY,19843
|
|
41
|
-
trilogy/core/processing/node_generators/unnest_node.py,sha256=aZeixbOzMtXi7BPahKr9bOkIhTciyD9Klsj0kZ56F6s,2189
|
|
42
|
-
trilogy/core/processing/node_generators/window_node.py,sha256=LSlXe41elFGVRlxRX3MEFimhduGn3o5WE0kLx2JtA4M,3322
|
|
43
|
-
trilogy/core/processing/nodes/__init__.py,sha256=jyduHk96j5fpju72sc8swOiBjR3Md866kt8JZGkp3ZU,4866
|
|
44
|
-
trilogy/core/processing/nodes/base_node.py,sha256=11Evv2LErwlzCU9ebLWlzldz7VbVMgYiR_sUkVYylKQ,13916
|
|
45
|
-
trilogy/core/processing/nodes/filter_node.py,sha256=DBOSGFfkiILrZa1BlLv2uxUSkgWtSIKiZplqyKXPjg8,2132
|
|
46
|
-
trilogy/core/processing/nodes/group_node.py,sha256=3zyEs11hv9CoGpO62cUKfClcS58clTUB0IMIkmOV998,6897
|
|
47
|
-
trilogy/core/processing/nodes/merge_node.py,sha256=bn7CwZwbYFx-OjNLb9oQuYL_abwAAd_KSSYJGFSEiP8,15022
|
|
48
|
-
trilogy/core/processing/nodes/select_node_v2.py,sha256=yoU2PWu-BkiUDECd7V7CKAPjznB_LObpl52HU9Sk5Yc,7433
|
|
49
|
-
trilogy/core/processing/nodes/unnest_node.py,sha256=mAmFluzm2yeeiQ6NfIB7BU_8atRGh-UJfPf9ROwbhr8,2152
|
|
50
|
-
trilogy/core/processing/nodes/window_node.py,sha256=X7qxLUKd3tekjUUsmH_4vz5b-U89gMnGd04VBxuu2Ns,1280
|
|
51
|
-
trilogy/dialect/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
52
|
-
trilogy/dialect/base.py,sha256=kQek_ufZC9HDVOKlWasvIx6xyew8wv3JNIU6r53_IR4,32842
|
|
53
|
-
trilogy/dialect/bigquery.py,sha256=15KJ-cOpBlk9O7FPviPgmg8xIydJeKx7WfmL3SSsPE8,2953
|
|
54
|
-
trilogy/dialect/common.py,sha256=QCsqo5morOOL6kwaCYh1RBmaInaoPI6lKtzdgroWvuM,3440
|
|
55
|
-
trilogy/dialect/config.py,sha256=tLVEMctaTDhUgARKXUNfHUcIolGaALkQ0RavUvXAY4w,2994
|
|
56
|
-
trilogy/dialect/duckdb.py,sha256=u_gpL35kouWxoBLas1h0ABYY2QzlVtEh22hm5h0lCOM,3182
|
|
57
|
-
trilogy/dialect/enums.py,sha256=4NdpsydBpDn6jnh0JzFz5VvQEtnShErWtWHVyT6TNpw,3948
|
|
58
|
-
trilogy/dialect/postgres.py,sha256=ev1RJZsC8BB3vJSxJ4q-TTYqZ4Hk1NXUtuRkLrQEBX0,3254
|
|
59
|
-
trilogy/dialect/presto.py,sha256=2Rs53UfPxKU0rJTcEbiS-Lxm-CDiqUGojh7yRpQgyRE,3416
|
|
60
|
-
trilogy/dialect/snowflake.py,sha256=_Bf4XO7-nImMv9XCSsTfVM3g2f_KHdO17VTa9J-HgSM,2989
|
|
61
|
-
trilogy/dialect/sql_server.py,sha256=OtXbm1v6NIGyXeC5i18ojUvLeqescc_Pbv3EASUBB94,3074
|
|
62
|
-
trilogy/hooks/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
63
|
-
trilogy/hooks/base_hook.py,sha256=Xkb-A2qCHozYjum0A36zOy5PwTVwrP3NLDF0U2GpgHo,1100
|
|
64
|
-
trilogy/hooks/graph_hook.py,sha256=onHvMQPwj_KOS3HOTpRFiy7QLLKAiycq2MzJ_Q0Oh5Y,2467
|
|
65
|
-
trilogy/hooks/query_debugger.py,sha256=NDChfkPmmW-KINa4TaQmDe_adGiwsKFdGLDSYpbodeU,4282
|
|
66
|
-
trilogy/metadata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
67
|
-
trilogy/parsing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
68
|
-
trilogy/parsing/common.py,sha256=-4LM71ocidA8DI2RngqFEOmhzBrIt8VdBTO4x2BpD8E,9502
|
|
69
|
-
trilogy/parsing/config.py,sha256=Z-DaefdKhPDmSXLgg5V4pebhSB0h590vI0_VtHnlukI,111
|
|
70
|
-
trilogy/parsing/exceptions.py,sha256=92E5i2frv5hj9wxObJZsZqj5T6bglvPzvdvco_vW1Zk,38
|
|
71
|
-
trilogy/parsing/helpers.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
72
|
-
trilogy/parsing/parse_engine.py,sha256=_DNpdZOQoZZio_mW8nxcCKoJHny7hnQQtOGG1msoCFU,63265
|
|
73
|
-
trilogy/parsing/render.py,sha256=Gy_6wVYPwYLf35Iota08sbqveuWILtUhI8MYStcvtJM,12174
|
|
74
|
-
trilogy/parsing/trilogy.lark,sha256=-9y4oVAIlKi-6pE58G4RwGGTBeG7P3T_V4gV8mILd8w,11549
|
|
75
|
-
trilogy/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
76
|
-
trilogy/scripts/trilogy.py,sha256=PHxvv6f2ODv0esyyhWxlARgra8dVhqQhYl0lTrSyVNo,3729
|
|
77
|
-
pytrilogy-0.0.2.15.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
|
|
78
|
-
pytrilogy-0.0.2.15.dist-info/METADATA,sha256=u9wzNtWDmhTjKdAmncckSd1lZv1j_Rkefq6kxifmvCI,7907
|
|
79
|
-
pytrilogy-0.0.2.15.dist-info/WHEEL,sha256=GV9aMThwP_4oNCtvEC2ec3qUYutgWeAzklro_0m4WJQ,91
|
|
80
|
-
pytrilogy-0.0.2.15.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
|
|
81
|
-
pytrilogy-0.0.2.15.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
|
|
82
|
-
pytrilogy-0.0.2.15.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|