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.

@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: pytrilogy
3
- Version: 0.0.2.53
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 simplicity with the power and iteratability 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.
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
- '''SELECT
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
- Python "semantic layers" are tools for defining data access to a warehouse in a more abstract way.
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=Lk4_BtdYZRgbnCEPlrwMwxuAnk38-h_AXRKbFgToSN8,291
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=CSmQyEXE6EZ4XFYuQQITUHuWXxXGo9AL4UsTnu0404A,7159
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=1d9Li5gQymKDl2U9PWweEYctxgxaKORIggsQhUibefM,162044
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=2TeYHZzUkLbQh0602IWilLd2yyfoorhc1zAkpTluyHU,9033
34
- trilogy/core/processing/node_generators/filter_node.py,sha256=NAI5gDQap8ygk905lTuPh6UxoWy--PpChEjgSJl5jKA,7567
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=cLEfhjQPpnxc1_34j_1A7lmsGcbi89F2cMqDYls4Oh0,5199
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=ZVYF8sOzSa1bkUp7y_fwEmRPfsnVJ2kmbUuUg8TpHRY,12062
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=GgImj-mtDHOctDQWC1SDuSANPR0S_nXTJOhmOuHyD5g,69332
78
- trilogy/parsing/render.py,sha256=5daSXbg3_RUQlHYyqqQQxneLc6NJvEKQ8n6Dm_kW75g,16225
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.53.dist-info/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
83
- pytrilogy-0.0.2.53.dist-info/METADATA,sha256=V5THusUgv5Bloqno7bceW4wdgvzC_qPHOTfAJou6HMQ,8426
84
- pytrilogy-0.0.2.53.dist-info/WHEEL,sha256=PZUExdf71Ui_so67QXpySuHtCi3-J3wvF4ORK6k_S8U,91
85
- pytrilogy-0.0.2.53.dist-info/entry_points.txt,sha256=0petKryjvvtEfTlbZC1AuMFumH_WQ9v8A19LvoS6G6c,54
86
- pytrilogy-0.0.2.53.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
87
- pytrilogy-0.0.2.53.dist-info/RECORD,,
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
@@ -4,6 +4,6 @@ from trilogy.dialect.enums import Dialects
4
4
  from trilogy.executor import Executor
5
5
  from trilogy.parser import parse
6
6
 
7
- __version__ = "0.0.2.53"
7
+ __version__ = "0.0.2.55"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -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=(concept,),
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=(concept,),
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=(concept,),
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[Tuple["Concept", ...]] = None
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
- tuple(x.with_merge(source, target, modifiers) for x in self.keys)
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
- tuple([x.with_namespace(namespace) for x in self.keys])
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 = tuple(grain_components)
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 = tuple(new_lineage.by)
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
- if item.keys and not all(c in components for c in item.keys):
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.address for x in components]),
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=key,
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.raise_undefined(self.address, line_no=self.line_no)
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.address in orig for k in x.keys]):
4525
- x.keys = tuple(
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 = tuple()
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.address for y in x.keys])
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
- list(immediate_parent.keys)
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, Tuple[Concept, ...] | None]:
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, ...]) -> Tuple[Concept, ...]:
113
- final_keys: List[Concept] = []
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 += concept_list_to_keys(concept.keys)
116
+ final_keys += list(concept.keys)
117
117
  elif concept.purpose != Purpose.PROPERTY:
118
- final_keys.append(concept)
119
- return tuple(unique(final_keys, "address"))
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.address in pconcepts for c in sub.keys]):
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 = tuple(concepts_to_grain_concepts(key_grain, environment))
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 (parent.content,)
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=tuple(parent.by) if parent.by else 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
@@ -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 or {}
247
- self.text_lookup: dict[Path | str, str] = text_lookup or {}
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
- namespaces = set([x.namespace for x in keys])
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={**self.parsed, **{self.parse_address: self}},
908
- tokens={**self.tokens, **{token_lookup: raw_tokens}},
909
- text_lookup={**self.text_lookup, **{token_lookup: text}},
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(f"Unable to import file {target}, parsing error: {e}")
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, nparser.environment, imps)
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
- raise e
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.name].append(concept)
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 key in keys:
110
- output_concepts += [key]
111
- output_concepts += properties.get(key.name, [])
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
- keys = ",".join([self.to_string(key) for key in concept.keys])
260
- output = f"{concept.purpose.value} <{keys}>.{namespace}{concept.name} {self.to_string(concept.datatype)};"
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("\\", ".")