pytrilogy 0.0.2.49__py3-none-any.whl → 0.0.2.51__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.49.dist-info → pytrilogy-0.0.2.51.dist-info}/METADATA +1 -1
- {pytrilogy-0.0.2.49.dist-info → pytrilogy-0.0.2.51.dist-info}/RECORD +43 -41
- trilogy/__init__.py +1 -1
- trilogy/core/enums.py +11 -0
- trilogy/core/functions.py +4 -1
- trilogy/core/internal.py +5 -1
- trilogy/core/models.py +135 -263
- trilogy/core/processing/concept_strategies_v3.py +14 -7
- trilogy/core/processing/node_generators/basic_node.py +7 -3
- trilogy/core/processing/node_generators/common.py +8 -5
- trilogy/core/processing/node_generators/filter_node.py +5 -8
- trilogy/core/processing/node_generators/group_node.py +24 -9
- trilogy/core/processing/node_generators/group_to_node.py +0 -2
- trilogy/core/processing/node_generators/multiselect_node.py +4 -5
- trilogy/core/processing/node_generators/node_merge_node.py +14 -3
- trilogy/core/processing/node_generators/rowset_node.py +3 -5
- trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
- trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +203 -0
- trilogy/core/processing/node_generators/select_merge_node.py +153 -66
- trilogy/core/processing/node_generators/union_node.py +0 -1
- trilogy/core/processing/node_generators/unnest_node.py +0 -2
- trilogy/core/processing/node_generators/window_node.py +0 -2
- trilogy/core/processing/nodes/base_node.py +2 -36
- trilogy/core/processing/nodes/filter_node.py +0 -3
- trilogy/core/processing/nodes/group_node.py +19 -13
- trilogy/core/processing/nodes/merge_node.py +2 -5
- trilogy/core/processing/nodes/select_node_v2.py +0 -4
- trilogy/core/processing/nodes/union_node.py +0 -3
- trilogy/core/processing/nodes/unnest_node.py +0 -3
- trilogy/core/processing/nodes/window_node.py +0 -3
- trilogy/core/processing/utility.py +3 -0
- trilogy/core/query_processor.py +0 -1
- trilogy/dialect/base.py +14 -2
- trilogy/dialect/duckdb.py +7 -0
- trilogy/hooks/graph_hook.py +17 -1
- trilogy/parsing/common.py +68 -17
- trilogy/parsing/parse_engine.py +70 -20
- trilogy/parsing/render.py +8 -1
- trilogy/parsing/trilogy.lark +3 -1
- {pytrilogy-0.0.2.49.dist-info → pytrilogy-0.0.2.51.dist-info}/LICENSE.md +0 -0
- {pytrilogy-0.0.2.49.dist-info → pytrilogy-0.0.2.51.dist-info}/WHEEL +0 -0
- {pytrilogy-0.0.2.49.dist-info → pytrilogy-0.0.2.51.dist-info}/entry_points.txt +0 -0
- {pytrilogy-0.0.2.49.dist-info → pytrilogy-0.0.2.51.dist-info}/top_level.txt +0 -0
trilogy/core/models.py
CHANGED
|
@@ -5,6 +5,7 @@ import hashlib
|
|
|
5
5
|
import os
|
|
6
6
|
from abc import ABC
|
|
7
7
|
from collections import UserDict, UserList, defaultdict
|
|
8
|
+
from datetime import date, datetime
|
|
8
9
|
from enum import Enum
|
|
9
10
|
from functools import cached_property
|
|
10
11
|
from pathlib import Path
|
|
@@ -35,7 +36,6 @@ from pydantic import (
|
|
|
35
36
|
ConfigDict,
|
|
36
37
|
Field,
|
|
37
38
|
ValidationInfo,
|
|
38
|
-
ValidatorFunctionWrapHandler,
|
|
39
39
|
computed_field,
|
|
40
40
|
field_validator,
|
|
41
41
|
)
|
|
@@ -408,31 +408,6 @@ class Metadata(BaseModel):
|
|
|
408
408
|
concept_source: ConceptSource = ConceptSource.MANUAL
|
|
409
409
|
|
|
410
410
|
|
|
411
|
-
def lineage_validator(
|
|
412
|
-
v: Any, handler: ValidatorFunctionWrapHandler, info: ValidationInfo
|
|
413
|
-
) -> Union[Function, WindowItem, FilterItem, AggregateWrapper]:
|
|
414
|
-
if v and not isinstance(v, (Function, WindowItem, FilterItem, AggregateWrapper)):
|
|
415
|
-
raise ValueError(v)
|
|
416
|
-
return v
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
def empty_grain() -> Grain:
|
|
420
|
-
return Grain(components=[])
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
class MultiLineage(BaseModel):
|
|
424
|
-
lineages: list[
|
|
425
|
-
Union[
|
|
426
|
-
Function,
|
|
427
|
-
WindowItem,
|
|
428
|
-
FilterItem,
|
|
429
|
-
AggregateWrapper,
|
|
430
|
-
RowsetItem,
|
|
431
|
-
MultiSelectStatement,
|
|
432
|
-
]
|
|
433
|
-
]
|
|
434
|
-
|
|
435
|
-
|
|
436
411
|
class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
|
|
437
412
|
name: str
|
|
438
413
|
datatype: DataType | ListType | StructType | MapType | NumericType
|
|
@@ -549,28 +524,22 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
|
|
|
549
524
|
values = info.data
|
|
550
525
|
if not v and values.get("purpose", None) == Purpose.KEY:
|
|
551
526
|
v = Grain(
|
|
552
|
-
components=
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
name=values["name"],
|
|
556
|
-
datatype=values["datatype"],
|
|
557
|
-
purpose=values["purpose"],
|
|
558
|
-
grain=Grain(),
|
|
559
|
-
)
|
|
560
|
-
]
|
|
527
|
+
components={
|
|
528
|
+
f'{values.get("namespace", DEFAULT_NAMESPACE)}.{values["name"]}'
|
|
529
|
+
}
|
|
561
530
|
)
|
|
562
531
|
elif (
|
|
563
532
|
"lineage" in values
|
|
564
533
|
and isinstance(values["lineage"], AggregateWrapper)
|
|
565
534
|
and values["lineage"].by
|
|
566
535
|
):
|
|
567
|
-
v = Grain(components=values["lineage"].by)
|
|
536
|
+
v = Grain(components={c.address for c in values["lineage"].by})
|
|
568
537
|
elif not v:
|
|
569
|
-
v = Grain(components=
|
|
538
|
+
v = Grain(components=set())
|
|
570
539
|
elif isinstance(v, Grain):
|
|
571
|
-
|
|
540
|
+
pass
|
|
572
541
|
elif isinstance(v, Concept):
|
|
573
|
-
v = Grain(components=
|
|
542
|
+
v = Grain(components={v.address})
|
|
574
543
|
elif isinstance(v, dict):
|
|
575
544
|
v = Grain.model_validate(v)
|
|
576
545
|
else:
|
|
@@ -593,8 +562,8 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
|
|
|
593
562
|
)
|
|
594
563
|
|
|
595
564
|
def __str__(self):
|
|
596
|
-
grain =
|
|
597
|
-
return f"{self.namespace}.{self.name}
|
|
565
|
+
grain = str(self.grain) if self.grain else "Grain<>"
|
|
566
|
+
return f"{self.namespace}.{self.name}@{grain}"
|
|
598
567
|
|
|
599
568
|
@cached_property
|
|
600
569
|
def address(self) -> str:
|
|
@@ -619,10 +588,6 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
|
|
|
619
588
|
return f"{self.namespace.replace('.','_')}_{self.name.replace('.','_')}"
|
|
620
589
|
return self.name.replace(".", "_")
|
|
621
590
|
|
|
622
|
-
@property
|
|
623
|
-
def grain_components(self) -> List["Concept"]:
|
|
624
|
-
return self.grain.components_copy if self.grain else []
|
|
625
|
-
|
|
626
591
|
def with_namespace(self, namespace: str) -> Self:
|
|
627
592
|
if namespace == self.namespace:
|
|
628
593
|
return self
|
|
@@ -635,7 +600,7 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
|
|
|
635
600
|
grain=(
|
|
636
601
|
self.grain.with_namespace(namespace)
|
|
637
602
|
if self.grain
|
|
638
|
-
else Grain(components=
|
|
603
|
+
else Grain(components=set())
|
|
639
604
|
),
|
|
640
605
|
namespace=(
|
|
641
606
|
namespace + "." + self.namespace
|
|
@@ -661,7 +626,7 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
|
|
|
661
626
|
new_lineage = self.lineage.with_select_context(
|
|
662
627
|
local_concepts=local_concepts, grain=grain, environment=environment
|
|
663
628
|
)
|
|
664
|
-
final_grain = self.grain
|
|
629
|
+
final_grain = self.grain or grain
|
|
665
630
|
keys = (
|
|
666
631
|
tuple(
|
|
667
632
|
[
|
|
@@ -673,9 +638,10 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
|
|
|
673
638
|
else None
|
|
674
639
|
)
|
|
675
640
|
if self.is_aggregate and isinstance(new_lineage, Function):
|
|
676
|
-
|
|
641
|
+
grain_components = [environment.concepts[c] for c in grain.components]
|
|
642
|
+
new_lineage = AggregateWrapper(function=new_lineage, by=grain_components)
|
|
677
643
|
final_grain = grain
|
|
678
|
-
keys = tuple(
|
|
644
|
+
keys = tuple(grain_components)
|
|
679
645
|
elif (
|
|
680
646
|
self.is_aggregate and not keys and isinstance(new_lineage, AggregateWrapper)
|
|
681
647
|
):
|
|
@@ -696,15 +662,13 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
|
|
|
696
662
|
)
|
|
697
663
|
|
|
698
664
|
def with_grain(self, grain: Optional["Grain"] = None) -> Self:
|
|
699
|
-
if not all([isinstance(x, Concept) for x in self.keys or []]):
|
|
700
|
-
raise ValueError(f"Invalid keys {self.keys} for concept {self.address}")
|
|
701
665
|
return self.__class__(
|
|
702
666
|
name=self.name,
|
|
703
667
|
datatype=self.datatype,
|
|
704
668
|
purpose=self.purpose,
|
|
705
669
|
metadata=self.metadata,
|
|
706
670
|
lineage=self.lineage,
|
|
707
|
-
grain=grain if grain else Grain(components=
|
|
671
|
+
grain=grain if grain else Grain(components=set()),
|
|
708
672
|
namespace=self.namespace,
|
|
709
673
|
keys=self.keys,
|
|
710
674
|
modifiers=self.modifiers,
|
|
@@ -715,7 +679,7 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
|
|
|
715
679
|
def _with_default_grain(self) -> Self:
|
|
716
680
|
if self.purpose == Purpose.KEY:
|
|
717
681
|
# we need to make this abstract
|
|
718
|
-
grain = Grain(components=
|
|
682
|
+
grain = Grain(components={self.address})
|
|
719
683
|
elif self.purpose == Purpose.PROPERTY:
|
|
720
684
|
components = []
|
|
721
685
|
if self.keys:
|
|
@@ -727,12 +691,15 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
|
|
|
727
691
|
components += item.sources
|
|
728
692
|
else:
|
|
729
693
|
components += item.sources
|
|
730
|
-
|
|
694
|
+
# TODO: set synonyms
|
|
695
|
+
grain = Grain(
|
|
696
|
+
components=set([x.address for x in components]),
|
|
697
|
+
) # synonym_set=generate_concept_synonyms(components))
|
|
731
698
|
elif self.purpose == Purpose.METRIC:
|
|
732
699
|
grain = Grain()
|
|
733
700
|
elif self.purpose == Purpose.CONSTANT:
|
|
734
701
|
if self.derivation != PurposeLineage.CONSTANT:
|
|
735
|
-
grain = Grain(components=
|
|
702
|
+
grain = Grain(components={self.address})
|
|
736
703
|
else:
|
|
737
704
|
grain = self.grain
|
|
738
705
|
else:
|
|
@@ -854,7 +821,7 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
|
|
|
854
821
|
elif self.derivation == PurposeLineage.AGGREGATE:
|
|
855
822
|
# if it's an aggregate grouped over all rows
|
|
856
823
|
# there is only one row left and it's fine to cross_join
|
|
857
|
-
if all([x.
|
|
824
|
+
if all([x.endswith(ALL_ROWS_CONCEPT) for x in self.grain.components]):
|
|
858
825
|
return Granularity.SINGLE_ROW
|
|
859
826
|
elif self.namespace == INTERNAL_NAMESPACE and self.name == ALL_ROWS_CONCEPT:
|
|
860
827
|
return Granularity.SINGLE_ROW
|
|
@@ -892,7 +859,7 @@ class Concept(Mergeable, Namespaced, SelectContext, BaseModel):
|
|
|
892
859
|
metadata=self.metadata,
|
|
893
860
|
lineage=FilterItem(content=self, where=WhereClause(conditional=condition)),
|
|
894
861
|
keys=(self.keys if self.purpose == Purpose.PROPERTY else None),
|
|
895
|
-
grain=self.grain if self.grain else Grain(components=
|
|
862
|
+
grain=self.grain if self.grain else Grain(components=set()),
|
|
896
863
|
namespace=self.namespace,
|
|
897
864
|
modifiers=self.modifiers,
|
|
898
865
|
pseudonyms=self.pseudonyms,
|
|
@@ -910,161 +877,123 @@ class ConceptRef(BaseModel):
|
|
|
910
877
|
return environment.concepts.__getitem__(self.address, self.line_no)
|
|
911
878
|
|
|
912
879
|
|
|
913
|
-
class Grain(
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
where_clause: Optional[WhereClause] = Field(default=None)
|
|
880
|
+
class Grain(Namespaced, BaseModel):
|
|
881
|
+
components: set[str] = Field(default_factory=set)
|
|
882
|
+
where_clause: Optional["WhereClause"] = None
|
|
917
883
|
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
v2 = unique(v, "address")
|
|
927
|
-
final: List[Concept] = []
|
|
928
|
-
for sub in v2:
|
|
929
|
-
if sub.purpose in (Purpose.PROPERTY, Purpose.METRIC) and sub.keys:
|
|
930
|
-
if all([c in v2 for c in sub.keys]):
|
|
931
|
-
continue
|
|
932
|
-
final.append(sub)
|
|
933
|
-
v2 = sorted(final, key=lambda x: x.name)
|
|
934
|
-
return v2
|
|
935
|
-
|
|
936
|
-
def with_select_context(
|
|
937
|
-
self, local_concepts: dict[str, Concept], grain: Grain, environment: Environment
|
|
938
|
-
):
|
|
939
|
-
if self.nested:
|
|
940
|
-
return self
|
|
941
|
-
return Grain(
|
|
942
|
-
components=[
|
|
943
|
-
x.with_select_context(local_concepts, grain, environment)
|
|
944
|
-
for x in self.components
|
|
945
|
-
],
|
|
946
|
-
where_clause=self.where_clause,
|
|
947
|
-
nested=self.nested,
|
|
948
|
-
)
|
|
884
|
+
def with_merge(self, source: Concept, target: Concept, modifiers: List[Modifier]):
|
|
885
|
+
new_components = set()
|
|
886
|
+
for c in self.components:
|
|
887
|
+
if c == source.address:
|
|
888
|
+
new_components.add(target.address)
|
|
889
|
+
else:
|
|
890
|
+
new_components.add(c)
|
|
891
|
+
return Grain(components=new_components)
|
|
949
892
|
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
893
|
+
@classmethod
|
|
894
|
+
def from_concepts(
|
|
895
|
+
cls,
|
|
896
|
+
concepts: List[Concept],
|
|
953
897
|
environment: Environment | None = None,
|
|
898
|
+
where_clause: WhereClause | None = None,
|
|
954
899
|
) -> "Grain":
|
|
900
|
+
from trilogy.parsing.common import concepts_to_grain_concepts
|
|
901
|
+
|
|
955
902
|
return Grain(
|
|
956
|
-
components=
|
|
957
|
-
|
|
903
|
+
components={
|
|
904
|
+
c.address
|
|
905
|
+
for c in concepts_to_grain_concepts(concepts, environment=environment)
|
|
906
|
+
},
|
|
907
|
+
where_clause=where_clause,
|
|
958
908
|
)
|
|
959
909
|
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
910
|
+
def with_namespace(self, namespace: str) -> "Grain":
|
|
911
|
+
return Grain(
|
|
912
|
+
components={address_with_namespace(c, namespace) for c in self.components},
|
|
913
|
+
where_clause=(
|
|
914
|
+
self.where_clause.with_namespace(namespace)
|
|
915
|
+
if self.where_clause
|
|
916
|
+
else None
|
|
917
|
+
),
|
|
918
|
+
)
|
|
963
919
|
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
920
|
+
@field_validator("components", mode="before")
|
|
921
|
+
def component_validator(cls, v, info: ValidationInfo):
|
|
922
|
+
output = set()
|
|
923
|
+
if isinstance(v, list):
|
|
924
|
+
for vc in v:
|
|
925
|
+
if isinstance(vc, Concept):
|
|
926
|
+
output.add(vc.address)
|
|
927
|
+
elif isinstance(vc, ConceptRef):
|
|
928
|
+
output.add(vc.address)
|
|
929
|
+
else:
|
|
930
|
+
output.add(vc)
|
|
967
931
|
else:
|
|
968
|
-
|
|
969
|
-
if
|
|
970
|
-
|
|
971
|
-
|
|
932
|
+
output = v
|
|
933
|
+
if not isinstance(output, set):
|
|
934
|
+
raise ValueError(f"Invalid grain component {output}, is not set")
|
|
935
|
+
if not all(isinstance(x, str) for x in output):
|
|
936
|
+
raise ValueError(f"Invalid component {output}")
|
|
937
|
+
return output
|
|
972
938
|
|
|
973
|
-
def
|
|
939
|
+
def __add__(self, other: "Grain") -> "Grain":
|
|
940
|
+
where = self.where_clause
|
|
941
|
+
if other.where_clause:
|
|
942
|
+
if not self.where_clause:
|
|
943
|
+
where = other.where_clause
|
|
944
|
+
elif not other.where_clause == self.where_clause:
|
|
945
|
+
raise NotImplementedError(
|
|
946
|
+
f"Cannot merge grains with where clauses, self {self.where_clause} other {other.where_clause}"
|
|
947
|
+
)
|
|
974
948
|
return Grain(
|
|
975
|
-
components=
|
|
976
|
-
nested=self.nested,
|
|
949
|
+
components=self.components.union(other.components), where_clause=where
|
|
977
950
|
)
|
|
978
951
|
|
|
979
|
-
def
|
|
980
|
-
self, source: Concept, target: Concept, modifiers: List[Modifier]
|
|
981
|
-
) -> "Grain":
|
|
952
|
+
def __sub__(self, other: "Grain") -> "Grain":
|
|
982
953
|
return Grain(
|
|
983
|
-
components=
|
|
984
|
-
|
|
985
|
-
],
|
|
986
|
-
nested=self.nested,
|
|
954
|
+
components=self.components.difference(other.components),
|
|
955
|
+
where_clause=self.where_clause,
|
|
987
956
|
)
|
|
988
957
|
|
|
989
958
|
@property
|
|
990
959
|
def abstract(self):
|
|
991
960
|
return not self.components or all(
|
|
992
|
-
[c.
|
|
961
|
+
[c.endswith(ALL_ROWS_CONCEPT) for c in self.components]
|
|
993
962
|
)
|
|
994
963
|
|
|
995
|
-
@property
|
|
996
|
-
def synonym_set(self) -> set[str]:
|
|
997
|
-
base = []
|
|
998
|
-
for x in self.components_copy:
|
|
999
|
-
if isinstance(x.lineage, RowsetItem):
|
|
1000
|
-
base.append(x.lineage.content.address)
|
|
1001
|
-
for c in x.lineage.content.pseudonyms:
|
|
1002
|
-
base.append(c)
|
|
1003
|
-
else:
|
|
1004
|
-
base.append(x.address)
|
|
1005
|
-
for c in x.pseudonyms:
|
|
1006
|
-
base.append(c)
|
|
1007
|
-
return set(base)
|
|
1008
|
-
|
|
1009
|
-
@property
|
|
1010
|
-
def set(self) -> set[str]:
|
|
1011
|
-
base = []
|
|
1012
|
-
for x in self.components_copy:
|
|
1013
|
-
if isinstance(x.lineage, RowsetItem):
|
|
1014
|
-
base.append(x.lineage.content.address)
|
|
1015
|
-
else:
|
|
1016
|
-
base.append(x.address)
|
|
1017
|
-
return set(base)
|
|
1018
|
-
|
|
1019
964
|
def __eq__(self, other: object):
|
|
1020
965
|
if isinstance(other, list):
|
|
1021
|
-
|
|
966
|
+
if not all([isinstance(c, Concept) for c in other]):
|
|
967
|
+
return False
|
|
968
|
+
return self.components == set([c.address for c in other])
|
|
1022
969
|
if not isinstance(other, Grain):
|
|
1023
970
|
return False
|
|
1024
|
-
if self.
|
|
1025
|
-
return True
|
|
1026
|
-
elif self.synonym_set == other.synonym_set:
|
|
971
|
+
if self.components == other.components:
|
|
1027
972
|
return True
|
|
1028
973
|
return False
|
|
1029
974
|
|
|
1030
975
|
def issubset(self, other: "Grain"):
|
|
1031
|
-
return self.
|
|
976
|
+
return self.components.issubset(other.components)
|
|
1032
977
|
|
|
1033
978
|
def union(self, other: "Grain"):
|
|
1034
|
-
addresses = self.
|
|
1035
|
-
|
|
1036
|
-
return Grain(
|
|
1037
|
-
components=[c for c in self.components if c.address in addresses]
|
|
1038
|
-
+ [c for c in other.components if c.address in addresses]
|
|
1039
|
-
)
|
|
979
|
+
addresses = self.components.union(other.components)
|
|
980
|
+
return Grain(components=addresses, where_clause=self.where_clause)
|
|
1040
981
|
|
|
1041
982
|
def isdisjoint(self, other: "Grain"):
|
|
1042
|
-
return self.
|
|
983
|
+
return self.components.isdisjoint(other.components)
|
|
1043
984
|
|
|
1044
985
|
def intersection(self, other: "Grain") -> "Grain":
|
|
1045
|
-
intersection = self.
|
|
1046
|
-
components
|
|
1047
|
-
return Grain(components=components)
|
|
986
|
+
intersection = self.components.intersection(other.components)
|
|
987
|
+
return Grain(components=intersection)
|
|
1048
988
|
|
|
1049
|
-
def
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
for c in components:
|
|
1058
|
-
if c.purpose == Purpose.PROPERTY and not any(
|
|
1059
|
-
[key in base_components for key in (c.keys or [])]
|
|
1060
|
-
):
|
|
1061
|
-
base_components.append(c)
|
|
1062
|
-
elif (
|
|
1063
|
-
c.purpose == Purpose.CONSTANT
|
|
1064
|
-
and not c.derivation == PurposeLineage.CONSTANT
|
|
1065
|
-
):
|
|
1066
|
-
base_components.append(c)
|
|
1067
|
-
return Grain(components=base_components)
|
|
989
|
+
def __str__(self):
|
|
990
|
+
if self.abstract:
|
|
991
|
+
base = "Grain<Abstract>"
|
|
992
|
+
else:
|
|
993
|
+
base = "Grain<" + ",".join([c for c in sorted(list(self.components))]) + ">"
|
|
994
|
+
if self.where_clause:
|
|
995
|
+
base += f"|{str(self.where_clause)}"
|
|
996
|
+
return base
|
|
1068
997
|
|
|
1069
998
|
def __radd__(self, other) -> "Grain":
|
|
1070
999
|
if other == 0:
|
|
@@ -1264,6 +1193,8 @@ class Function(Mergeable, Namespaced, SelectContext, BaseModel):
|
|
|
1264
1193
|
int,
|
|
1265
1194
|
float,
|
|
1266
1195
|
str,
|
|
1196
|
+
date,
|
|
1197
|
+
datetime,
|
|
1267
1198
|
MapWrapper[Any, Any],
|
|
1268
1199
|
DataType,
|
|
1269
1200
|
ListType,
|
|
@@ -1752,6 +1683,7 @@ class SelectStatement(HasUUID, Mergeable, Namespaced, SelectTypeMixin, BaseModel
|
|
|
1752
1683
|
local_concepts: Annotated[
|
|
1753
1684
|
EnvironmentConceptDict, PlainValidator(validate_concepts)
|
|
1754
1685
|
] = Field(default_factory=EnvironmentConceptDict)
|
|
1686
|
+
grain: Grain = Field(default_factory=Grain)
|
|
1755
1687
|
|
|
1756
1688
|
def validate_syntax(self, environment: Environment):
|
|
1757
1689
|
if self.where_clause:
|
|
@@ -1803,15 +1735,6 @@ class SelectStatement(HasUUID, Mergeable, Namespaced, SelectTypeMixin, BaseModel
|
|
|
1803
1735
|
|
|
1804
1736
|
return render_query(self)
|
|
1805
1737
|
|
|
1806
|
-
def __init__(self, *args, **kwargs) -> None:
|
|
1807
|
-
super().__init__(*args, **kwargs)
|
|
1808
|
-
for nitem in self.selection:
|
|
1809
|
-
if not isinstance(nitem.content, Concept):
|
|
1810
|
-
continue
|
|
1811
|
-
if nitem.content.grain == Grain():
|
|
1812
|
-
if nitem.content.derivation == PurposeLineage.AGGREGATE:
|
|
1813
|
-
nitem.content = nitem.content.with_grain(self.grain)
|
|
1814
|
-
|
|
1815
1738
|
@field_validator("selection", mode="before")
|
|
1816
1739
|
@classmethod
|
|
1817
1740
|
def selection_validation(cls, v):
|
|
@@ -1883,9 +1806,7 @@ class SelectStatement(HasUUID, Mergeable, Namespaced, SelectTypeMixin, BaseModel
|
|
|
1883
1806
|
|
|
1884
1807
|
@property
|
|
1885
1808
|
def all_components(self) -> List[Concept]:
|
|
1886
|
-
return
|
|
1887
|
-
self.input_components + self.output_components + self.grain.components_copy
|
|
1888
|
-
)
|
|
1809
|
+
return self.input_components + self.output_components
|
|
1889
1810
|
|
|
1890
1811
|
def to_datasource(
|
|
1891
1812
|
self,
|
|
@@ -1935,55 +1856,6 @@ class SelectStatement(HasUUID, Mergeable, Namespaced, SelectTypeMixin, BaseModel
|
|
|
1935
1856
|
column.concept = column.concept.with_grain(new_datasource.grain)
|
|
1936
1857
|
return new_datasource
|
|
1937
1858
|
|
|
1938
|
-
@property
|
|
1939
|
-
def grain(self) -> "Grain":
|
|
1940
|
-
output = []
|
|
1941
|
-
for item in self.output_components:
|
|
1942
|
-
if item.purpose == Purpose.KEY:
|
|
1943
|
-
output.append(item)
|
|
1944
|
-
# if self.where_clause:
|
|
1945
|
-
# for item in self.where_clause.concept_arguments:
|
|
1946
|
-
# if item.purpose == Purpose.KEY:
|
|
1947
|
-
# output.append(item)
|
|
1948
|
-
# elif item.purpose == Purpose.PROPERTY and item.grain:
|
|
1949
|
-
# output += item.grain.components
|
|
1950
|
-
# TODO: handle other grain cases
|
|
1951
|
-
# new if block by design
|
|
1952
|
-
# add back any purpose that is not at the grain
|
|
1953
|
-
# if a query already has the key of the property in the grain
|
|
1954
|
-
# we want to group to that grain and ignore the property, which is a derivation
|
|
1955
|
-
# otherwise, we need to include property as the group by
|
|
1956
|
-
for item in self.output_components:
|
|
1957
|
-
if (
|
|
1958
|
-
item.purpose == Purpose.PROPERTY
|
|
1959
|
-
and item.grain
|
|
1960
|
-
and (
|
|
1961
|
-
not item.grain.components
|
|
1962
|
-
or not item.grain.issubset(
|
|
1963
|
-
Grain(components=unique(output, "address"))
|
|
1964
|
-
)
|
|
1965
|
-
)
|
|
1966
|
-
):
|
|
1967
|
-
output.append(item)
|
|
1968
|
-
if (
|
|
1969
|
-
item.purpose == Purpose.CONSTANT
|
|
1970
|
-
and item.derivation != PurposeLineage.CONSTANT
|
|
1971
|
-
and item.grain
|
|
1972
|
-
and (
|
|
1973
|
-
not item.grain.components
|
|
1974
|
-
or not item.grain.issubset(
|
|
1975
|
-
Grain(components=unique(output, "address"))
|
|
1976
|
-
)
|
|
1977
|
-
)
|
|
1978
|
-
):
|
|
1979
|
-
output.append(item)
|
|
1980
|
-
# TODO: explore implicit filtering more
|
|
1981
|
-
# if self.where_clause.conditional and self.where_clause_category == SelectFiltering.IMPLICIT:
|
|
1982
|
-
# output =[x.with_filter(self.where_clause.conditional) for x in output]
|
|
1983
|
-
return Grain(
|
|
1984
|
-
components=unique(output, "address"), where_clause=self.where_clause
|
|
1985
|
-
)
|
|
1986
|
-
|
|
1987
1859
|
def with_namespace(self, namespace: str) -> "SelectStatement":
|
|
1988
1860
|
return SelectStatement(
|
|
1989
1861
|
selection=[c.with_namespace(namespace) for c in self.selection],
|
|
@@ -2200,7 +2072,7 @@ def safe_grain(v) -> Grain:
|
|
|
2200
2072
|
elif isinstance(v, Grain):
|
|
2201
2073
|
return v
|
|
2202
2074
|
elif not v:
|
|
2203
|
-
return Grain(components=
|
|
2075
|
+
return Grain(components=set())
|
|
2204
2076
|
else:
|
|
2205
2077
|
raise ValueError(f"Invalid input type to safe_grain {type(v)}")
|
|
2206
2078
|
|
|
@@ -2232,7 +2104,7 @@ class Datasource(HasUUID, Namespaced, BaseModel):
|
|
|
2232
2104
|
columns: List[ColumnAssignment]
|
|
2233
2105
|
address: Union[Address, str]
|
|
2234
2106
|
grain: Grain = Field(
|
|
2235
|
-
default_factory=lambda: Grain(components=
|
|
2107
|
+
default_factory=lambda: Grain(components=set()), validate_default=True
|
|
2236
2108
|
)
|
|
2237
2109
|
namespace: Optional[str] = Field(default=DEFAULT_NAMESPACE, validate_default=True)
|
|
2238
2110
|
metadata: DatasourceMetadata = Field(
|
|
@@ -2320,8 +2192,8 @@ class Datasource(HasUUID, Namespaced, BaseModel):
|
|
|
2320
2192
|
grain: Grain = safe_grain(v)
|
|
2321
2193
|
if not grain.components:
|
|
2322
2194
|
columns: List[ColumnAssignment] = values.get("columns", [])
|
|
2323
|
-
grain = Grain(
|
|
2324
|
-
|
|
2195
|
+
grain = Grain.from_concepts(
|
|
2196
|
+
[
|
|
2325
2197
|
c.concept.with_grain(Grain())
|
|
2326
2198
|
for c in columns
|
|
2327
2199
|
if c.concept.purpose == Purpose.KEY
|
|
@@ -2755,15 +2627,11 @@ class QueryDatasource(BaseModel):
|
|
|
2755
2627
|
@property
|
|
2756
2628
|
def identifier(self) -> str:
|
|
2757
2629
|
filters = abs(hash(str(self.condition))) if self.condition else ""
|
|
2758
|
-
grain = "_".join(
|
|
2759
|
-
[str(c.address).replace(".", "_") for c in self.grain.components]
|
|
2760
|
-
)
|
|
2761
|
-
# partial = "_".join([str(c.address).replace(".", "_") for c in self.partial_concepts])
|
|
2630
|
+
grain = "_".join([str(c).replace(".", "_") for c in self.grain.components])
|
|
2762
2631
|
return (
|
|
2763
2632
|
"_join_".join([d.identifier for d in self.datasources])
|
|
2764
2633
|
+ (f"_at_{grain}" if grain else "_at_abstract")
|
|
2765
2634
|
+ (f"_filtered_by_{filters}" if filters else "")
|
|
2766
|
-
# + (f"_partial_{partial}" if partial else "")
|
|
2767
2635
|
)
|
|
2768
2636
|
|
|
2769
2637
|
def get_alias(
|
|
@@ -3102,12 +2970,16 @@ class CTE(BaseModel):
|
|
|
3102
2970
|
for cte in self.parent_ctes:
|
|
3103
2971
|
if address in cte.output_columns:
|
|
3104
2972
|
match = [x for x in cte.output_columns if x.address == address].pop()
|
|
3105
|
-
|
|
2973
|
+
if match:
|
|
2974
|
+
return match
|
|
3106
2975
|
|
|
3107
2976
|
for array in [self.source.input_concepts, self.source.output_concepts]:
|
|
3108
2977
|
match_list = [x for x in array if x.address == address]
|
|
3109
2978
|
if match_list:
|
|
3110
2979
|
return match_list.pop()
|
|
2980
|
+
match_list = [x for x in self.output_columns if x.address == address]
|
|
2981
|
+
if match_list:
|
|
2982
|
+
return match_list.pop()
|
|
3111
2983
|
return None
|
|
3112
2984
|
|
|
3113
2985
|
def get_alias(self, concept: Concept, source: str | None = None) -> str:
|
|
@@ -3116,8 +2988,10 @@ class CTE(BaseModel):
|
|
|
3116
2988
|
if source and source != cte.name:
|
|
3117
2989
|
continue
|
|
3118
2990
|
return concept.safe_address
|
|
2991
|
+
|
|
3119
2992
|
try:
|
|
3120
2993
|
source = self.source.get_alias(concept, source=source)
|
|
2994
|
+
|
|
3121
2995
|
if not source:
|
|
3122
2996
|
raise ValueError("No source found")
|
|
3123
2997
|
return source
|
|
@@ -3130,7 +3004,8 @@ class CTE(BaseModel):
|
|
|
3130
3004
|
if len(self.source_map.get(c.address, [])) > 0:
|
|
3131
3005
|
return False
|
|
3132
3006
|
if c.derivation == PurposeLineage.ROWSET:
|
|
3133
|
-
|
|
3007
|
+
assert isinstance(c.lineage, RowsetItem)
|
|
3008
|
+
return check_is_not_in_group(c.lineage.content)
|
|
3134
3009
|
if c.derivation == PurposeLineage.CONSTANT:
|
|
3135
3010
|
return False
|
|
3136
3011
|
if c.purpose == Purpose.METRIC:
|
|
@@ -3327,7 +3202,6 @@ class UndefinedConcept(Concept, Mergeable, Namespaced):
|
|
|
3327
3202
|
if self.address in local_concepts:
|
|
3328
3203
|
rval = local_concepts[self.address]
|
|
3329
3204
|
rval = rval.with_select_context(local_concepts, grain, environment)
|
|
3330
|
-
|
|
3331
3205
|
return rval
|
|
3332
3206
|
environment.concepts.raise_undefined(self.address, line_no=self.line_no)
|
|
3333
3207
|
|
|
@@ -3868,6 +3742,8 @@ class Comparison(
|
|
|
3868
3742
|
float,
|
|
3869
3743
|
list,
|
|
3870
3744
|
bool,
|
|
3745
|
+
datetime,
|
|
3746
|
+
date,
|
|
3871
3747
|
Function,
|
|
3872
3748
|
Concept,
|
|
3873
3749
|
"Conditional",
|
|
@@ -3884,6 +3760,8 @@ class Comparison(
|
|
|
3884
3760
|
float,
|
|
3885
3761
|
list,
|
|
3886
3762
|
bool,
|
|
3763
|
+
date,
|
|
3764
|
+
datetime,
|
|
3887
3765
|
Concept,
|
|
3888
3766
|
Function,
|
|
3889
3767
|
"Conditional",
|
|
@@ -4455,7 +4333,7 @@ class AggregateWrapper(Mergeable, Namespaced, SelectContext, BaseModel):
|
|
|
4455
4333
|
self, local_concepts: dict[str, Concept], grain: Grain, environment: Environment
|
|
4456
4334
|
) -> AggregateWrapper:
|
|
4457
4335
|
if not self.by:
|
|
4458
|
-
by = grain.
|
|
4336
|
+
by = [environment.concepts[c] for c in grain.components]
|
|
4459
4337
|
else:
|
|
4460
4338
|
by = [
|
|
4461
4339
|
x.with_select_context(local_concepts, grain, environment)
|
|
@@ -4504,16 +4382,6 @@ class WhereClause(Mergeable, ConceptArgs, Namespaced, SelectContext, BaseModel):
|
|
|
4504
4382
|
)
|
|
4505
4383
|
)
|
|
4506
4384
|
|
|
4507
|
-
@property
|
|
4508
|
-
def grain(self) -> Grain:
|
|
4509
|
-
output = []
|
|
4510
|
-
for item in self.input:
|
|
4511
|
-
if item.purpose == Purpose.KEY:
|
|
4512
|
-
output.append(item)
|
|
4513
|
-
elif item.purpose == Purpose.PROPERTY:
|
|
4514
|
-
output += item.grain.components if item.grain else []
|
|
4515
|
-
return Grain(components=list(set(output)))
|
|
4516
|
-
|
|
4517
4385
|
@property
|
|
4518
4386
|
def components(self):
|
|
4519
4387
|
from trilogy.core.processing.utility import decompose_condition
|
|
@@ -4649,7 +4517,7 @@ class RowsetDerivationStatement(HasUUID, Namespaced, BaseModel):
|
|
|
4649
4517
|
)
|
|
4650
4518
|
orig[orig_concept.address] = new_concept
|
|
4651
4519
|
output.append(new_concept)
|
|
4652
|
-
default_grain = Grain(
|
|
4520
|
+
default_grain = Grain.from_concepts([*output])
|
|
4653
4521
|
# remap everything to the properties of the rowset
|
|
4654
4522
|
for x in output:
|
|
4655
4523
|
if x.keys:
|
|
@@ -4661,9 +4529,9 @@ class RowsetDerivationStatement(HasUUID, Namespaced, BaseModel):
|
|
|
4661
4529
|
# TODO: fix this up
|
|
4662
4530
|
x.keys = tuple()
|
|
4663
4531
|
for x in output:
|
|
4664
|
-
if all([c
|
|
4532
|
+
if all([c in orig for c in x.grain.components]):
|
|
4665
4533
|
x.grain = Grain(
|
|
4666
|
-
components=
|
|
4534
|
+
components={orig[c].address for c in x.grain.components}
|
|
4667
4535
|
)
|
|
4668
4536
|
else:
|
|
4669
4537
|
x.grain = default_grain
|
|
@@ -5008,5 +4876,9 @@ def arg_to_datatype(arg) -> DataType | ListType | StructType | MapType | Numeric
|
|
|
5008
4876
|
return ListType(type=wrapper.type)
|
|
5009
4877
|
elif isinstance(arg, MapWrapper):
|
|
5010
4878
|
return MapType(key_type=arg.key_type, value_type=arg.value_type)
|
|
4879
|
+
elif isinstance(arg, datetime):
|
|
4880
|
+
return DataType.DATETIME
|
|
4881
|
+
elif isinstance(arg, date):
|
|
4882
|
+
return DataType.DATE
|
|
5011
4883
|
else:
|
|
5012
4884
|
raise ValueError(f"Cannot parse arg datatype for arg of raw type {type(arg)}")
|