pytrilogy 0.0.2.53__py3-none-any.whl → 0.0.2.55__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.53.dist-info → pytrilogy-0.0.2.55.dist-info}/METADATA +32 -12
- {pytrilogy-0.0.2.53.dist-info → pytrilogy-0.0.2.55.dist-info}/RECORD +15 -15
- trilogy/__init__.py +1 -1
- trilogy/core/environment_helpers.py +9 -3
- trilogy/core/models.py +32 -56
- trilogy/core/processing/node_generators/common.py +4 -3
- trilogy/core/processing/node_generators/filter_node.py +3 -2
- trilogy/core/processing/node_generators/rowset_node.py +0 -1
- trilogy/parsing/common.py +14 -12
- trilogy/parsing/parse_engine.py +22 -19
- trilogy/parsing/render.py +26 -7
- {pytrilogy-0.0.2.53.dist-info → pytrilogy-0.0.2.55.dist-info}/LICENSE.md +0 -0
- {pytrilogy-0.0.2.53.dist-info → pytrilogy-0.0.2.55.dist-info}/WHEEL +0 -0
- {pytrilogy-0.0.2.53.dist-info → pytrilogy-0.0.2.55.dist-info}/entry_points.txt +0 -0
- {pytrilogy-0.0.2.53.dist-info → pytrilogy-0.0.2.55.dist-info}/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: pytrilogy
|
|
3
|
-
Version: 0.0.2.
|
|
3
|
+
Version: 0.0.2.55
|
|
4
4
|
Summary: Declarative, typed query language that compiles to SQL.
|
|
5
5
|
Home-page:
|
|
6
6
|
Author:
|
|
@@ -34,7 +34,7 @@ Requires-Dist: snowflake-sqlalchemy; extra == "snowflake"
|
|
|
34
34
|
|
|
35
35
|
pytrilogy is an experimental implementation of the Trilogy language, a higher-level SQL that replaces tables/joins with a lightweight semantic binding layer.
|
|
36
36
|
|
|
37
|
-
Trilogy looks like SQL, but simpler. It's a modern SQL refresh targeted at SQL lovers who want reusability and
|
|
37
|
+
Trilogy looks like SQL, but simpler. It's a modern SQL refresh targeted at SQL lovers who want more reusability and composability without losing the expressiveness and iterative value of SQL. It compiles to SQL - making it easy to debug or integrate into existing workflows - and can be run against any supported SQL backend.
|
|
38
38
|
|
|
39
39
|
> [!TIP]
|
|
40
40
|
> To get an overview of the language and run interactive examples, head to the [documentation](https://trilogydata.dev/).
|
|
@@ -193,11 +193,12 @@ address `bigquery-public-data.usa_names.usa_1910_2013`;
|
|
|
193
193
|
executor = Dialects.BIGQUERY.default_executor(environment=environment)
|
|
194
194
|
|
|
195
195
|
results = executor.execute_text(
|
|
196
|
-
'''
|
|
197
|
-
name,
|
|
198
|
-
sum(yearly_name_count) -> name_count
|
|
196
|
+
'''
|
|
199
197
|
WHERE
|
|
200
198
|
name = 'Elvis'
|
|
199
|
+
SELECT
|
|
200
|
+
name,
|
|
201
|
+
sum(yearly_name_count) -> name_count
|
|
201
202
|
ORDER BY
|
|
202
203
|
name_count desc
|
|
203
204
|
LIMIT 10;
|
|
@@ -270,9 +271,11 @@ Please open an issue first to discuss what you would like to change, and then cr
|
|
|
270
271
|
## Similar in space
|
|
271
272
|
Trilogy combines two aspects; a semantic layer and a query language. Examples of both are linked below:
|
|
272
273
|
|
|
273
|
-
|
|
274
|
+
"semantic layers" are tools for defining an metadata layer above a SQL/warehouse base to enable higher level abstractions.
|
|
274
275
|
|
|
275
276
|
- [metricsflow](https://github.com/dbt-labs/metricflow)
|
|
277
|
+
- [cube](https://github.com/cube-js/cube)
|
|
278
|
+
- [zillion](https://github.com/totalhack/zillion)
|
|
276
279
|
|
|
277
280
|
"Better SQL" has been a popular space. We believe Trilogy takes a different approach then the following,
|
|
278
281
|
but all are worth checking out. Please open PRs/comment for anything missed!
|
|
@@ -321,11 +324,14 @@ address <table>;
|
|
|
321
324
|
Primary acces
|
|
322
325
|
|
|
323
326
|
```sql
|
|
324
|
-
select
|
|
325
|
-
<concept>,
|
|
326
|
-
<concept>+1 -> <alias>
|
|
327
327
|
WHERE
|
|
328
328
|
<concept> = <value>
|
|
329
|
+
select
|
|
330
|
+
<concept>,
|
|
331
|
+
<concept>+1 -> <alias>,
|
|
332
|
+
...
|
|
333
|
+
HAVING
|
|
334
|
+
<alias> = <value2>
|
|
329
335
|
ORDER BY
|
|
330
336
|
<concept> asc|desc
|
|
331
337
|
;
|
|
@@ -337,11 +343,13 @@ Reusable virtual set of rows. Useful for windows, filtering.
|
|
|
337
343
|
|
|
338
344
|
```sql
|
|
339
345
|
with <alias> as
|
|
340
|
-
select
|
|
341
|
-
<concept>,
|
|
342
|
-
<concept>+1 -> <alias>
|
|
343
346
|
WHERE
|
|
344
347
|
<concept> = <value>
|
|
348
|
+
select
|
|
349
|
+
<concept>,
|
|
350
|
+
<concept>+1 -> <alias>,
|
|
351
|
+
...
|
|
352
|
+
|
|
345
353
|
|
|
346
354
|
select <alias>.<concept>;
|
|
347
355
|
|
|
@@ -357,6 +365,18 @@ persist <alias> as <table_name> from
|
|
|
357
365
|
<select>;
|
|
358
366
|
```
|
|
359
367
|
|
|
368
|
+
#### COPY
|
|
369
|
+
|
|
370
|
+
Currently supported target types are <CSV>, though backend support may vary.
|
|
371
|
+
|
|
372
|
+
```sql
|
|
373
|
+
COPY INTO <TARGET_TYPE> '<target_path>' FROM SELECT
|
|
374
|
+
<concept>, ...
|
|
375
|
+
ORDER BY
|
|
376
|
+
<concept>, ...
|
|
377
|
+
;
|
|
378
|
+
```
|
|
379
|
+
|
|
360
380
|
#### SHOW
|
|
361
381
|
|
|
362
382
|
Return generated SQL without executing.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
trilogy/__init__.py,sha256=
|
|
1
|
+
trilogy/__init__.py,sha256=JnykGbcn-V3aG4x6q9pNpBv7a-8Pr93-61ey6CjNHBA,291
|
|
2
2
|
trilogy/compiler.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
3
|
trilogy/constants.py,sha256=qZ1d0hoKPPV2HHCoFwPYTVB7b6bXjpWvXd3lE-zEhy8,1494
|
|
4
4
|
trilogy/engine.py,sha256=yOPnR7XCjWG82Gym_LLZBkYKKJdLCvqdCyt8zguNcnM,1103
|
|
@@ -10,13 +10,13 @@ trilogy/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
|
10
10
|
trilogy/core/constants.py,sha256=7XaCpZn5mQmjTobbeBn56SzPWq9eMNDfzfsRU-fP0VE,171
|
|
11
11
|
trilogy/core/enums.py,sha256=6pGjEXNJPB1ngbDQRJjxRi4NmKM8NZQ5-iwnZhrdo5U,7281
|
|
12
12
|
trilogy/core/env_processor.py,sha256=Pt4lmJfbShBbeSe5M7_FrTk5krrOziiAA__Slnettvc,2585
|
|
13
|
-
trilogy/core/environment_helpers.py,sha256=
|
|
13
|
+
trilogy/core/environment_helpers.py,sha256=GExsRXghccqKOIiEjkCjtDVDz0PNTE5OSRPFelPa5eU,7279
|
|
14
14
|
trilogy/core/ergonomics.py,sha256=ASLDd0RqKWrZiG3XcKHo8nyTjaB_8xfE9t4NZ1UvGpc,1639
|
|
15
15
|
trilogy/core/exceptions.py,sha256=1c1lQCwSw4_5CQS3q7scOkXU8GQvullJXfPHubprl90,617
|
|
16
16
|
trilogy/core/functions.py,sha256=hDlwLxQUskT9iRcIic1lfACQnxMLNM5ASdHRPi0ghyw,10835
|
|
17
17
|
trilogy/core/graph_models.py,sha256=mameUTiuCajtihDw_2-W218xyJlvTusOWrEKP1yAWgk,2003
|
|
18
18
|
trilogy/core/internal.py,sha256=FQWbuETKPfzjALMmdXJwlOMlESfm2Z5gmErSsq3BX9c,1173
|
|
19
|
-
trilogy/core/models.py,sha256=
|
|
19
|
+
trilogy/core/models.py,sha256=J1s7EAbJIoHseoz1D1iP1sAbEDNt66cinS2VE-zLp7g,161242
|
|
20
20
|
trilogy/core/optimization.py,sha256=Jy3tVJNeqhpK6VSyTvgIWKCao6y-VCZ7mYA69MIF6L0,7989
|
|
21
21
|
trilogy/core/query_processor.py,sha256=V-TqybYO0kCY8O7Nk58OBhb7_eRPs_EqAwaQv-EYLSY,18615
|
|
22
22
|
trilogy/core/optimizations/__init__.py,sha256=EBanqTXEzf1ZEYjAneIWoIcxtMDite5-n2dQ5xcfUtg,356
|
|
@@ -30,13 +30,13 @@ trilogy/core/processing/graph_utils.py,sha256=stbYnDxnK-1kbo9L4XNU85FQhWCP-oZYO7
|
|
|
30
30
|
trilogy/core/processing/utility.py,sha256=STqSHP8fWTVmaIUCfHAb9Hke_fzOG2pTbmWIdYS4cvc,18787
|
|
31
31
|
trilogy/core/processing/node_generators/__init__.py,sha256=s_YV1OYc336DuS9591259qjI_K_CtOCuhkf4t2aOgYs,733
|
|
32
32
|
trilogy/core/processing/node_generators/basic_node.py,sha256=dz7i0BSn4qRv6SBIS_JnVAm09-nkNizoAHrznmqnJXY,3074
|
|
33
|
-
trilogy/core/processing/node_generators/common.py,sha256=
|
|
34
|
-
trilogy/core/processing/node_generators/filter_node.py,sha256=
|
|
33
|
+
trilogy/core/processing/node_generators/common.py,sha256=dHycWu9iiRxH3WIkkyibsnYD5mJfXvdEOhsTvyaO8fg,9128
|
|
34
|
+
trilogy/core/processing/node_generators/filter_node.py,sha256=zDgPfj-LkjkLKasH6Obftw-ojxVQi1d984nvxwTLPEU,7634
|
|
35
35
|
trilogy/core/processing/node_generators/group_node.py,sha256=k57SVWHSVvTqCd47tyLUGCsSZaP7UQqMCJYTSz1S7oQ,5566
|
|
36
36
|
trilogy/core/processing/node_generators/group_to_node.py,sha256=8ToptIWQoJttquEPrRTMvU33jCJQI-VJxVObN8W8QJk,2511
|
|
37
37
|
trilogy/core/processing/node_generators/multiselect_node.py,sha256=se-cHRYRPskxq2Wq9bw5LkUFSCN1rhk8_05-OTezLz0,6421
|
|
38
38
|
trilogy/core/processing/node_generators/node_merge_node.py,sha256=3GzuiTiorFVe9MyLhoz2PDyI0x9XL7bQ8ucEbV54le8,14627
|
|
39
|
-
trilogy/core/processing/node_generators/rowset_node.py,sha256=
|
|
39
|
+
trilogy/core/processing/node_generators/rowset_node.py,sha256=ekrXWFu4ga3VR59Ux870w5gSmzFPC9WjIRuyB4yFqag,5138
|
|
40
40
|
trilogy/core/processing/node_generators/select_merge_node.py,sha256=YW0H81IpE9B6f0SK75QH2DVSfr8d3oA9AbbqP44Jhnc,15746
|
|
41
41
|
trilogy/core/processing/node_generators/select_node.py,sha256=bjTylBa-vYbmzpuSpphmIo_Oi78YZpI8ppHnN9KDYDk,1795
|
|
42
42
|
trilogy/core/processing/node_generators/union_node.py,sha256=MfJjF2m0ARl0oUH9QT1awzPv0e3yA3mXK1XqAvUTgKw,2504
|
|
@@ -70,18 +70,18 @@ trilogy/hooks/graph_hook.py,sha256=c-vC-IXoJ_jDmKQjxQyIxyXPOuUcLIURB573gCsAfzQ,2
|
|
|
70
70
|
trilogy/hooks/query_debugger.py,sha256=FoDh2bu2NiwLusVhKa5El_l8EKaqfET7zn55GP0TkOE,4644
|
|
71
71
|
trilogy/metadata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
72
72
|
trilogy/parsing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
73
|
-
trilogy/parsing/common.py,sha256=
|
|
73
|
+
trilogy/parsing/common.py,sha256=iPpnSkiKUtoSTsfrMCHZOexu9H6-eIQznbVsKNEPbT8,12032
|
|
74
74
|
trilogy/parsing/config.py,sha256=Z-DaefdKhPDmSXLgg5V4pebhSB0h590vI0_VtHnlukI,111
|
|
75
75
|
trilogy/parsing/exceptions.py,sha256=92E5i2frv5hj9wxObJZsZqj5T6bglvPzvdvco_vW1Zk,38
|
|
76
76
|
trilogy/parsing/helpers.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
|
|
77
|
-
trilogy/parsing/parse_engine.py,sha256=
|
|
78
|
-
trilogy/parsing/render.py,sha256=
|
|
77
|
+
trilogy/parsing/parse_engine.py,sha256=JGL2qm4L14ytkkB4FiwaXvC1EYJSOK9J0eemi6L94Bw,69222
|
|
78
|
+
trilogy/parsing/render.py,sha256=o4C12a407iZvlRGUJDiuJUezrLLo4QEaLtu60ZQX3gk,16942
|
|
79
79
|
trilogy/parsing/trilogy.lark,sha256=EazfEvYPuvkPkNjUnVzFi0uD9baavugbSI8CyfawShk,12573
|
|
80
80
|
trilogy/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
81
81
|
trilogy/scripts/trilogy.py,sha256=DQDW81E5mDMWFP8oPw8q-IyrR2JGxQSDWgUWe2VTSRQ,3731
|
|
82
|
-
pytrilogy-0.0.2.
|
|
83
|
-
pytrilogy-0.0.2.
|
|
84
|
-
pytrilogy-0.0.2.
|
|
85
|
-
pytrilogy-0.0.2.
|
|
86
|
-
pytrilogy-0.0.2.
|
|
87
|
-
pytrilogy-0.0.2.
|
|
82
|
+
pytrilogy-0.0.2.55.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
|
|
83
|
+
pytrilogy-0.0.2.55.dist-info/METADATA,sha256=FFvNA0mm8yeWHphFZm0-LAIoPu_qbPSSYwTzoXVz1yI,8823
|
|
84
|
+
pytrilogy-0.0.2.55.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
|
|
85
|
+
pytrilogy-0.0.2.55.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
|
|
86
|
+
pytrilogy-0.0.2.55.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
|
|
87
|
+
pytrilogy-0.0.2.55.dist-info/RECORD,,
|
trilogy/__init__.py
CHANGED
|
@@ -50,7 +50,9 @@ def generate_date_concepts(concept: Concept, environment: Environment):
|
|
|
50
50
|
lineage=const_function,
|
|
51
51
|
grain=const_function.output_grain,
|
|
52
52
|
namespace=namespace,
|
|
53
|
-
keys=(
|
|
53
|
+
keys=set(
|
|
54
|
+
concept.address,
|
|
55
|
+
),
|
|
54
56
|
metadata=Metadata(
|
|
55
57
|
description=f"Auto-derived. Integer format. The {ftype.value} derived from {concept.name}, {base_description}",
|
|
56
58
|
line_number=base_line_number,
|
|
@@ -99,7 +101,9 @@ def generate_datetime_concepts(concept: Concept, environment: Environment):
|
|
|
99
101
|
lineage=const_function,
|
|
100
102
|
grain=const_function.output_grain,
|
|
101
103
|
namespace=namespace,
|
|
102
|
-
keys=(
|
|
104
|
+
keys=set(
|
|
105
|
+
concept.address,
|
|
106
|
+
),
|
|
103
107
|
metadata=Metadata(
|
|
104
108
|
description=f"Auto-derived. Integer format. The {ftype.value} derived from {concept.name}, {base_description}",
|
|
105
109
|
line_number=base_line_number,
|
|
@@ -139,7 +143,9 @@ def generate_key_concepts(concept: Concept, environment: Environment):
|
|
|
139
143
|
lineage=const_function,
|
|
140
144
|
grain=const_function.output_grain,
|
|
141
145
|
namespace=namespace,
|
|
142
|
-
keys=
|
|
146
|
+
keys={
|
|
147
|
+
concept.address,
|
|
148
|
+
},
|
|
143
149
|
metadata=Metadata(
|
|
144
150
|
description=f"Auto-derived. Integer format. The {ftype.value} derived from {concept.name}, {base_description}",
|
|
145
151
|
line_number=base_line_number,
|
trilogy/core/models.py
CHANGED
|
@@ -427,12 +427,15 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
|
|
|
427
427
|
]
|
|
428
428
|
] = None
|
|
429
429
|
namespace: Optional[str] = Field(default=DEFAULT_NAMESPACE, validate_default=True)
|
|
430
|
-
keys: Optional[
|
|
430
|
+
keys: Optional[set[str]] = None
|
|
431
431
|
grain: "Grain" = Field(default=None, validate_default=True) # type: ignore
|
|
432
432
|
modifiers: List[Modifier] = Field(default_factory=list) # type: ignore
|
|
433
433
|
pseudonyms: set[str] = Field(default_factory=set)
|
|
434
434
|
_address_cache: str | None = None
|
|
435
435
|
|
|
436
|
+
def __init__(self, **data):
|
|
437
|
+
super().__init__(**data)
|
|
438
|
+
|
|
436
439
|
def duplicate(self) -> Concept:
|
|
437
440
|
return self.model_copy(deep=True)
|
|
438
441
|
|
|
@@ -480,7 +483,7 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
|
|
|
480
483
|
grain=self.grain.with_merge(source, target, modifiers),
|
|
481
484
|
namespace=self.namespace,
|
|
482
485
|
keys=(
|
|
483
|
-
|
|
486
|
+
set(x if x != source.address else target.address for x in self.keys)
|
|
484
487
|
if self.keys
|
|
485
488
|
else None
|
|
486
489
|
),
|
|
@@ -488,17 +491,6 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
|
|
|
488
491
|
pseudonyms=self.pseudonyms,
|
|
489
492
|
)
|
|
490
493
|
|
|
491
|
-
@field_validator("keys", mode="before")
|
|
492
|
-
@classmethod
|
|
493
|
-
def keys_validator(cls, v, info: ValidationInfo):
|
|
494
|
-
if v is None:
|
|
495
|
-
return v
|
|
496
|
-
if not isinstance(v, (list, tuple)):
|
|
497
|
-
raise ValueError(f"Keys must be a list or tuple, got {type(v)}")
|
|
498
|
-
if isinstance(v, list):
|
|
499
|
-
return tuple(v)
|
|
500
|
-
return v
|
|
501
|
-
|
|
502
494
|
@field_validator("namespace", mode="plain")
|
|
503
495
|
@classmethod
|
|
504
496
|
def namespace_validation(cls, v):
|
|
@@ -610,7 +602,7 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
|
|
|
610
602
|
else namespace
|
|
611
603
|
),
|
|
612
604
|
keys=(
|
|
613
|
-
|
|
605
|
+
set([address_with_namespace(x, namespace) for x in self.keys])
|
|
614
606
|
if self.keys
|
|
615
607
|
else None
|
|
616
608
|
),
|
|
@@ -627,25 +619,17 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
|
|
|
627
619
|
local_concepts=local_concepts, grain=grain, environment=environment
|
|
628
620
|
)
|
|
629
621
|
final_grain = self.grain or grain
|
|
630
|
-
keys =
|
|
631
|
-
tuple(
|
|
632
|
-
[
|
|
633
|
-
x.with_select_context(local_concepts, grain, environment)
|
|
634
|
-
for x in self.keys
|
|
635
|
-
]
|
|
636
|
-
)
|
|
637
|
-
if self.keys
|
|
638
|
-
else None
|
|
639
|
-
)
|
|
622
|
+
keys = self.keys if self.keys else None
|
|
640
623
|
if self.is_aggregate and isinstance(new_lineage, Function):
|
|
641
624
|
grain_components = [environment.concepts[c] for c in grain.components]
|
|
642
625
|
new_lineage = AggregateWrapper(function=new_lineage, by=grain_components)
|
|
643
626
|
final_grain = grain
|
|
644
|
-
keys =
|
|
627
|
+
keys = set(grain.components)
|
|
645
628
|
elif (
|
|
646
629
|
self.is_aggregate and not keys and isinstance(new_lineage, AggregateWrapper)
|
|
647
630
|
):
|
|
648
|
-
keys =
|
|
631
|
+
keys = set([x.address for x in new_lineage.by])
|
|
632
|
+
|
|
649
633
|
return self.__class__(
|
|
650
634
|
name=self.name,
|
|
651
635
|
datatype=self.datatype,
|
|
@@ -687,13 +671,10 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
|
|
|
687
671
|
if self.lineage:
|
|
688
672
|
for item in self.lineage.arguments:
|
|
689
673
|
if isinstance(item, Concept):
|
|
690
|
-
|
|
691
|
-
components += item.sources
|
|
692
|
-
else:
|
|
693
|
-
components += item.sources
|
|
674
|
+
components += [x.address for x in item.sources]
|
|
694
675
|
# TODO: set synonyms
|
|
695
676
|
grain = Grain(
|
|
696
|
-
components=set([x
|
|
677
|
+
components=set([x for x in components]),
|
|
697
678
|
) # synonym_set=generate_concept_synonyms(components))
|
|
698
679
|
elif self.purpose == Purpose.METRIC:
|
|
699
680
|
grain = Grain()
|
|
@@ -871,7 +852,7 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
|
|
|
871
852
|
|
|
872
853
|
class ConceptRef(BaseModel):
|
|
873
854
|
address: str
|
|
874
|
-
line_no: int
|
|
855
|
+
line_no: int | None = None
|
|
875
856
|
|
|
876
857
|
def hydrate(self, environment: Environment) -> Concept:
|
|
877
858
|
return environment.concepts.__getitem__(self.address, self.line_no)
|
|
@@ -1058,13 +1039,19 @@ class EnvironmentConceptDict(dict):
|
|
|
1058
1039
|
if DEFAULT_NAMESPACE + "." + key in self:
|
|
1059
1040
|
return self.__getitem__(DEFAULT_NAMESPACE + "." + key, line_no)
|
|
1060
1041
|
if not self.fail_on_missing:
|
|
1042
|
+
if "." in key:
|
|
1043
|
+
ns, rest = key.rsplit(".", 1)
|
|
1044
|
+
else:
|
|
1045
|
+
ns = DEFAULT_NAMESPACE
|
|
1046
|
+
rest = key
|
|
1061
1047
|
if key in self.undefined:
|
|
1062
1048
|
return self.undefined[key]
|
|
1063
1049
|
undefined = UndefinedConcept(
|
|
1064
|
-
name=
|
|
1050
|
+
name=rest,
|
|
1065
1051
|
line_no=line_no,
|
|
1066
1052
|
datatype=DataType.UNKNOWN,
|
|
1067
1053
|
purpose=Purpose.UNKNOWN,
|
|
1054
|
+
namespace=ns,
|
|
1068
1055
|
)
|
|
1069
1056
|
self.undefined[key] = undefined
|
|
1070
1057
|
return undefined
|
|
@@ -1365,18 +1352,6 @@ class Function(Mergeable, Namespaced, SelectContext, BaseModel):
|
|
|
1365
1352
|
base_grain += input.grain
|
|
1366
1353
|
return base_grain
|
|
1367
1354
|
|
|
1368
|
-
@property
|
|
1369
|
-
def output_keys(self) -> list[Concept]:
|
|
1370
|
-
# aggregates have an abstract grain
|
|
1371
|
-
components = []
|
|
1372
|
-
# scalars have implicit grain of all arguments
|
|
1373
|
-
for input in self.concept_arguments:
|
|
1374
|
-
if input.purpose == Purpose.KEY:
|
|
1375
|
-
components.append(input)
|
|
1376
|
-
elif input.keys:
|
|
1377
|
-
components += input.keys
|
|
1378
|
-
return list(set(components))
|
|
1379
|
-
|
|
1380
1355
|
|
|
1381
1356
|
class ConceptTransform(Namespaced, BaseModel):
|
|
1382
1357
|
function: Function | FilterItem | WindowItem | AggregateWrapper
|
|
@@ -3203,7 +3178,9 @@ class UndefinedConcept(Concept, Mergeable, Namespaced):
|
|
|
3203
3178
|
rval = local_concepts[self.address]
|
|
3204
3179
|
rval = rval.with_select_context(local_concepts, grain, environment)
|
|
3205
3180
|
return rval
|
|
3206
|
-
environment.concepts.
|
|
3181
|
+
if environment.concepts.fail_on_missing:
|
|
3182
|
+
environment.concepts.raise_undefined(self.address, line_no=self.line_no)
|
|
3183
|
+
return self
|
|
3207
3184
|
|
|
3208
3185
|
|
|
3209
3186
|
class EnvironmentDatasourceDict(dict):
|
|
@@ -3336,10 +3313,16 @@ class Environment(BaseModel):
|
|
|
3336
3313
|
|
|
3337
3314
|
@classmethod
|
|
3338
3315
|
def from_file(cls, path: str | Path) -> "Environment":
|
|
3316
|
+
if isinstance(path, str):
|
|
3317
|
+
path = Path(path)
|
|
3339
3318
|
with open(path, "r") as f:
|
|
3340
3319
|
read = f.read()
|
|
3341
3320
|
return Environment(working_path=Path(path).parent).parse(read)[0]
|
|
3342
3321
|
|
|
3322
|
+
@classmethod
|
|
3323
|
+
def from_string(cls, input: str) -> "Environment":
|
|
3324
|
+
return Environment().parse(input)[0]
|
|
3325
|
+
|
|
3343
3326
|
@classmethod
|
|
3344
3327
|
def from_cache(cls, path) -> Optional["Environment"]:
|
|
3345
3328
|
with open(path, "r") as f:
|
|
@@ -3645,11 +3628,6 @@ class Environment(BaseModel):
|
|
|
3645
3628
|
self.merge_concept(new_concept, current_concept, [])
|
|
3646
3629
|
else:
|
|
3647
3630
|
self.add_concept(current_concept, meta=meta, _ignore_cache=True)
|
|
3648
|
-
|
|
3649
|
-
# else:
|
|
3650
|
-
# self.add_concept(
|
|
3651
|
-
# current_concept, meta=meta, _ignore_cache=True
|
|
3652
|
-
# )
|
|
3653
3631
|
if not _ignore_cache:
|
|
3654
3632
|
self.gen_concept_list_caches()
|
|
3655
3633
|
return datasource
|
|
@@ -4521,13 +4499,11 @@ class RowsetDerivationStatement(HasUUID, Namespaced, BaseModel):
|
|
|
4521
4499
|
# remap everything to the properties of the rowset
|
|
4522
4500
|
for x in output:
|
|
4523
4501
|
if x.keys:
|
|
4524
|
-
if all([k
|
|
4525
|
-
x.keys =
|
|
4526
|
-
[orig[k.address] if k.address in orig else k for k in x.keys]
|
|
4527
|
-
)
|
|
4502
|
+
if all([k in orig for k in x.keys]):
|
|
4503
|
+
x.keys = set([orig[k].address if k in orig else k for k in x.keys])
|
|
4528
4504
|
else:
|
|
4529
4505
|
# TODO: fix this up
|
|
4530
|
-
x.keys =
|
|
4506
|
+
x.keys = set()
|
|
4531
4507
|
for x in output:
|
|
4532
4508
|
if all([c in orig for c in x.grain.components]):
|
|
4533
4509
|
x.grain = Grain(
|
|
@@ -26,6 +26,7 @@ def resolve_function_parent_concepts(
|
|
|
26
26
|
if not isinstance(concept.lineage, (Function, AggregateWrapper)):
|
|
27
27
|
raise ValueError(f"Concept {concept} lineage is not function or aggregate")
|
|
28
28
|
if concept.derivation == PurposeLineage.AGGREGATE:
|
|
29
|
+
base: list[Concept] = []
|
|
29
30
|
if not concept.grain.abstract:
|
|
30
31
|
base = concept.lineage.concept_arguments + [
|
|
31
32
|
environment.concepts[c] for c in concept.grain.components
|
|
@@ -41,7 +42,7 @@ def resolve_function_parent_concepts(
|
|
|
41
42
|
extra_property_grain = concept.lineage.concept_arguments
|
|
42
43
|
for x in extra_property_grain:
|
|
43
44
|
if isinstance(x, Concept) and x.purpose == Purpose.PROPERTY and x.keys:
|
|
44
|
-
base += x.keys
|
|
45
|
+
base += [environment.concepts[c] for c in x.keys]
|
|
45
46
|
return unique(base, "address")
|
|
46
47
|
# TODO: handle basic lineage chains?
|
|
47
48
|
return unique(concept.lineage.concept_arguments, "address")
|
|
@@ -81,7 +82,7 @@ def resolve_filter_parent_concepts(
|
|
|
81
82
|
and direct_parent.purpose == Purpose.PROPERTY
|
|
82
83
|
and direct_parent.keys
|
|
83
84
|
):
|
|
84
|
-
base_rows += direct_parent.keys
|
|
85
|
+
base_rows += [environment.concepts[c] for c in direct_parent.keys]
|
|
85
86
|
if concept.lineage.where.existence_arguments:
|
|
86
87
|
return (
|
|
87
88
|
concept.lineage.content,
|
|
@@ -106,7 +107,7 @@ def gen_property_enrichment_node(
|
|
|
106
107
|
for x in extra_properties:
|
|
107
108
|
if not x.keys:
|
|
108
109
|
raise SyntaxError(f"Property {x.address} missing keys in lookup")
|
|
109
|
-
keys = "-".join([y
|
|
110
|
+
keys = "-".join([y for y in x.keys])
|
|
110
111
|
required_keys[keys].add(x.address)
|
|
111
112
|
final_nodes = []
|
|
112
113
|
for _k, vs in required_keys.items():
|
|
@@ -138,7 +138,7 @@ def gen_filter_node(
|
|
|
138
138
|
)
|
|
139
139
|
parent.grain = Grain.from_concepts(
|
|
140
140
|
(
|
|
141
|
-
|
|
141
|
+
[environment.concepts[k] for k in immediate_parent.keys]
|
|
142
142
|
if immediate_parent.keys
|
|
143
143
|
else [immediate_parent]
|
|
144
144
|
)
|
|
@@ -146,7 +146,8 @@ def gen_filter_node(
|
|
|
146
146
|
x
|
|
147
147
|
for x in local_optional
|
|
148
148
|
if x.address in [y.address for y in parent.output_concepts]
|
|
149
|
-
]
|
|
149
|
+
],
|
|
150
|
+
environment=environment,
|
|
150
151
|
)
|
|
151
152
|
parent.rebuild_cache()
|
|
152
153
|
filter_node = parent
|
|
@@ -97,7 +97,6 @@ def gen_rowset_node(
|
|
|
97
97
|
possible_joins = concept_to_relevant_joins(
|
|
98
98
|
[x for x in node.output_concepts if x.derivation != PurposeLineage.ROWSET]
|
|
99
99
|
)
|
|
100
|
-
logger.info({x.address: x.keys for x in possible_joins})
|
|
101
100
|
if not possible_joins:
|
|
102
101
|
logger.info(
|
|
103
102
|
f"{padding(depth)}{LOGGER_PREFIX} no possible joins for rowset node to get {[x.address for x in local_optional]}; have {[x.address for x in node.output_concepts]}"
|
trilogy/parsing/common.py
CHANGED
|
@@ -100,7 +100,7 @@ def process_function_args(
|
|
|
100
100
|
|
|
101
101
|
def get_purpose_and_keys(
|
|
102
102
|
purpose: Purpose | None, args: Tuple[Concept, ...] | None
|
|
103
|
-
) -> Tuple[Purpose,
|
|
103
|
+
) -> Tuple[Purpose, set[str] | None]:
|
|
104
104
|
local_purpose = purpose or function_args_to_output_purpose(args)
|
|
105
105
|
if local_purpose in (Purpose.PROPERTY, Purpose.METRIC) and args:
|
|
106
106
|
keys = concept_list_to_keys(args)
|
|
@@ -109,14 +109,14 @@ def get_purpose_and_keys(
|
|
|
109
109
|
return local_purpose, keys
|
|
110
110
|
|
|
111
111
|
|
|
112
|
-
def concept_list_to_keys(concepts: Tuple[Concept, ...]) ->
|
|
113
|
-
final_keys: List[
|
|
112
|
+
def concept_list_to_keys(concepts: Tuple[Concept, ...]) -> set[str]:
|
|
113
|
+
final_keys: List[str] = []
|
|
114
114
|
for concept in concepts:
|
|
115
115
|
if concept.keys:
|
|
116
|
-
final_keys +=
|
|
116
|
+
final_keys += list(concept.keys)
|
|
117
117
|
elif concept.purpose != Purpose.PROPERTY:
|
|
118
|
-
final_keys.append(concept)
|
|
119
|
-
return
|
|
118
|
+
final_keys.append(concept.address)
|
|
119
|
+
return set(final_keys)
|
|
120
120
|
|
|
121
121
|
|
|
122
122
|
def constant_to_concept(
|
|
@@ -156,7 +156,7 @@ def concepts_to_grain_concepts(
|
|
|
156
156
|
final: List[Concept] = []
|
|
157
157
|
for sub in pconcepts:
|
|
158
158
|
if sub.purpose in (Purpose.PROPERTY, Purpose.METRIC) and sub.keys:
|
|
159
|
-
if any([c
|
|
159
|
+
if any([c in pconcepts for c in sub.keys]):
|
|
160
160
|
continue
|
|
161
161
|
if sub.purpose in (Purpose.METRIC,):
|
|
162
162
|
if all([c in pconcepts for c in sub.grain.components]):
|
|
@@ -191,13 +191,13 @@ def function_to_concept(
|
|
|
191
191
|
# if the function will create more rows, we don't know what grain this is at
|
|
192
192
|
grain = None
|
|
193
193
|
modifiers = get_upstream_modifiers(pkeys)
|
|
194
|
-
key_grain = []
|
|
194
|
+
key_grain: list[str] = []
|
|
195
195
|
for x in pkeys:
|
|
196
196
|
if x.keys:
|
|
197
197
|
key_grain += [*x.keys]
|
|
198
198
|
else:
|
|
199
|
-
key_grain.append(x)
|
|
200
|
-
keys =
|
|
199
|
+
key_grain.append(x.address)
|
|
200
|
+
keys = set(key_grain)
|
|
201
201
|
if not pkeys:
|
|
202
202
|
purpose = Purpose.CONSTANT
|
|
203
203
|
else:
|
|
@@ -248,7 +248,9 @@ def filter_item_to_concept(
|
|
|
248
248
|
keys=(
|
|
249
249
|
parent.content.keys
|
|
250
250
|
if parent.content.purpose == Purpose.PROPERTY
|
|
251
|
-
else
|
|
251
|
+
else {
|
|
252
|
+
parent.content.address,
|
|
253
|
+
}
|
|
252
254
|
),
|
|
253
255
|
grain=(
|
|
254
256
|
parent.content.grain
|
|
@@ -320,7 +322,7 @@ def agg_wrapper_to_concept(
|
|
|
320
322
|
lineage=parent,
|
|
321
323
|
grain=Grain.from_concepts(parent.by) if parent.by else Grain(),
|
|
322
324
|
namespace=namespace,
|
|
323
|
-
keys=
|
|
325
|
+
keys=set([x.address for x in parent.by]) if parent.by else keys,
|
|
324
326
|
modifiers=modifiers,
|
|
325
327
|
)
|
|
326
328
|
return out
|
trilogy/parsing/parse_engine.py
CHANGED
|
@@ -242,9 +242,11 @@ class ParseToObjects(Transformer):
|
|
|
242
242
|
self.environment: Environment = environment
|
|
243
243
|
self.parse_address: str = parse_address or SELF_LABEL
|
|
244
244
|
self.token_address: Path | str = token_address or SELF_LABEL
|
|
245
|
-
self.parsed: dict[str, ParseToObjects] = parsed if parsed else {}
|
|
246
|
-
self.tokens: dict[Path | str, ParseTree] = tokens
|
|
247
|
-
self.text_lookup: dict[Path | str, str] =
|
|
245
|
+
self.parsed: dict[str, ParseToObjects] = parsed if parsed is not None else {}
|
|
246
|
+
self.tokens: dict[Path | str, ParseTree] = tokens if tokens is not None else {}
|
|
247
|
+
self.text_lookup: dict[Path | str, str] = (
|
|
248
|
+
text_lookup if text_lookup is not None else {}
|
|
249
|
+
)
|
|
248
250
|
# we do a second pass to pick up circular dependencies
|
|
249
251
|
# after initial parsing
|
|
250
252
|
self.pass_count = 1
|
|
@@ -465,7 +467,7 @@ class ParseToObjects(Transformer):
|
|
|
465
467
|
metadata=metadata,
|
|
466
468
|
grain=Grain(components={x.address for x in parents}),
|
|
467
469
|
namespace=namespace,
|
|
468
|
-
keys=parents,
|
|
470
|
+
keys=set([x.address for x in parents]),
|
|
469
471
|
modifiers=modifiers,
|
|
470
472
|
)
|
|
471
473
|
self.environment.add_concept(concept, meta)
|
|
@@ -513,7 +515,8 @@ class ParseToObjects(Transformer):
|
|
|
513
515
|
# <abc.def,zef.gf>.property pattern
|
|
514
516
|
else:
|
|
515
517
|
keys, name = raw_name
|
|
516
|
-
|
|
518
|
+
keys = [x.address for x in keys]
|
|
519
|
+
namespaces = set([x.rsplit(".", 1)[0] for x in keys])
|
|
517
520
|
if not len(namespaces) == 1:
|
|
518
521
|
namespace = self.environment.namespace or DEFAULT_NAMESPACE
|
|
519
522
|
else:
|
|
@@ -894,31 +897,31 @@ class ParseToObjects(Transformer):
|
|
|
894
897
|
|
|
895
898
|
if cache_lookup in self.parsed:
|
|
896
899
|
nparser = self.parsed[cache_lookup]
|
|
900
|
+
new_env = nparser.environment
|
|
897
901
|
else:
|
|
898
902
|
try:
|
|
899
903
|
new_env = Environment(
|
|
900
904
|
working_path=dirname(target),
|
|
901
905
|
)
|
|
902
906
|
new_env.concepts.fail_on_missing = False
|
|
907
|
+
self.parsed[self.parse_address] = self
|
|
903
908
|
nparser = ParseToObjects(
|
|
904
909
|
environment=new_env,
|
|
905
910
|
parse_address=cache_lookup,
|
|
906
911
|
token_address=token_lookup,
|
|
907
|
-
parsed=
|
|
908
|
-
tokens=
|
|
909
|
-
text_lookup=
|
|
912
|
+
parsed=self.parsed,
|
|
913
|
+
tokens=self.tokens,
|
|
914
|
+
text_lookup=self.text_lookup,
|
|
910
915
|
)
|
|
911
916
|
nparser.transform(raw_tokens)
|
|
912
917
|
self.parsed[cache_lookup] = nparser
|
|
913
|
-
# add the parsed objects of the import in
|
|
914
|
-
self.parsed = {**self.parsed, **nparser.parsed}
|
|
915
|
-
self.tokens = {**self.tokens, **nparser.tokens}
|
|
916
|
-
self.text_lookup = {**self.text_lookup, **nparser.text_lookup}
|
|
917
918
|
except Exception as e:
|
|
918
|
-
raise ImportError(
|
|
919
|
+
raise ImportError(
|
|
920
|
+
f"Unable to import file {target}, parsing error: {e}"
|
|
921
|
+
) from e
|
|
919
922
|
|
|
920
923
|
imps = ImportStatement(alias=alias, path=Path(args[0]))
|
|
921
|
-
self.environment.add_import(alias,
|
|
924
|
+
self.environment.add_import(alias, new_env, imps)
|
|
922
925
|
return imps
|
|
923
926
|
|
|
924
927
|
@v_args(meta=True)
|
|
@@ -1026,6 +1029,7 @@ class ParseToObjects(Transformer):
|
|
|
1026
1029
|
having = arg
|
|
1027
1030
|
if not select_items:
|
|
1028
1031
|
raise ValueError("Malformed select, missing select items")
|
|
1032
|
+
|
|
1029
1033
|
output = SelectStatement(
|
|
1030
1034
|
selection=select_items,
|
|
1031
1035
|
where_clause=where,
|
|
@@ -1056,9 +1060,6 @@ class ParseToObjects(Transformer):
|
|
|
1056
1060
|
grain = Grain.from_concepts(
|
|
1057
1061
|
output.output_components, where_clause=output.where_clause
|
|
1058
1062
|
)
|
|
1059
|
-
print(
|
|
1060
|
-
f"end pass {parse_pass} grain {grain} from {output.output_components}"
|
|
1061
|
-
)
|
|
1062
1063
|
output.grain = grain
|
|
1063
1064
|
for item in select_items:
|
|
1064
1065
|
# we don't know the grain of an aggregate at assignment time
|
|
@@ -2000,8 +2001,10 @@ def unpack_visit_error(e: VisitError):
|
|
|
2000
2001
|
raise InvalidSyntaxException(
|
|
2001
2002
|
str(e.orig_exc) + " in " + str(e.rule) + f" Line: {e.obj.meta.line}"
|
|
2002
2003
|
)
|
|
2003
|
-
raise InvalidSyntaxException(str(e.orig_exc))
|
|
2004
|
-
|
|
2004
|
+
raise InvalidSyntaxException(str(e.orig_exc)).with_traceback(
|
|
2005
|
+
e.orig_exc.__traceback__
|
|
2006
|
+
)
|
|
2007
|
+
raise e.orig_exc
|
|
2005
2008
|
|
|
2006
2009
|
|
|
2007
2010
|
def parse_text_raw(text: str, environment: Optional[Environment] = None):
|
trilogy/parsing/render.py
CHANGED
|
@@ -18,6 +18,7 @@ from trilogy.core.models import (
|
|
|
18
18
|
Concept,
|
|
19
19
|
ConceptDeclarationStatement,
|
|
20
20
|
ConceptDerivation,
|
|
21
|
+
ConceptRef,
|
|
21
22
|
ConceptTransform,
|
|
22
23
|
Conditional,
|
|
23
24
|
CopyStatement,
|
|
@@ -98,17 +99,17 @@ class Renderer:
|
|
|
98
99
|
if concept.keys:
|
|
99
100
|
# avoid duplicate declarations
|
|
100
101
|
# but we need better composite key support
|
|
101
|
-
for key in concept.keys[:1]:
|
|
102
|
-
properties[key
|
|
102
|
+
for key in sorted(list(concept.keys))[:1]:
|
|
103
|
+
properties[key].append(concept)
|
|
103
104
|
else:
|
|
104
105
|
keys.append(concept)
|
|
105
106
|
else:
|
|
106
107
|
metrics.append(concept)
|
|
107
108
|
|
|
108
109
|
output_concepts = constants
|
|
109
|
-
for
|
|
110
|
-
output_concepts += [
|
|
111
|
-
output_concepts += properties.get(
|
|
110
|
+
for key_concept in keys:
|
|
111
|
+
output_concepts += [key_concept]
|
|
112
|
+
output_concepts += properties.get(key_concept.name, [])
|
|
112
113
|
output_concepts += metrics
|
|
113
114
|
|
|
114
115
|
rendered_concepts = [
|
|
@@ -256,8 +257,18 @@ class Renderer:
|
|
|
256
257
|
namespace = ""
|
|
257
258
|
if not concept.lineage:
|
|
258
259
|
if concept.purpose == Purpose.PROPERTY and concept.keys:
|
|
259
|
-
|
|
260
|
-
|
|
260
|
+
if len(concept.keys) == 1:
|
|
261
|
+
output = f"{concept.purpose.value} {self.to_string(ConceptRef(address=list(concept.keys)[0]))}.{namespace}{concept.name} {self.to_string(concept.datatype)};"
|
|
262
|
+
else:
|
|
263
|
+
keys = ",".join(
|
|
264
|
+
sorted(
|
|
265
|
+
list(
|
|
266
|
+
self.to_string(ConceptRef(address=x))
|
|
267
|
+
for x in concept.keys
|
|
268
|
+
)
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
output = f"{concept.purpose.value} <{keys}>.{namespace}{concept.name} {self.to_string(concept.datatype)};"
|
|
261
272
|
else:
|
|
262
273
|
output = f"{concept.purpose.value} {namespace}{concept.name} {self.to_string(concept.datatype)};"
|
|
263
274
|
else:
|
|
@@ -377,8 +388,16 @@ class Renderer:
|
|
|
377
388
|
|
|
378
389
|
@to_string.register
|
|
379
390
|
def _(self, arg: "FilterItem"):
|
|
391
|
+
|
|
380
392
|
return f"filter {self.to_string(arg.content)} where {self.to_string(arg.where)}"
|
|
381
393
|
|
|
394
|
+
@to_string.register
|
|
395
|
+
def _(self, arg: "ConceptRef"):
|
|
396
|
+
ns, base = arg.address.rsplit(".", 1)
|
|
397
|
+
if ns == DEFAULT_NAMESPACE:
|
|
398
|
+
return base
|
|
399
|
+
return arg.address
|
|
400
|
+
|
|
382
401
|
@to_string.register
|
|
383
402
|
def _(self, arg: "ImportStatement"):
|
|
384
403
|
path: str = str(arg.path).replace("\\", ".")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|