pytrilogy 0.0.3.91__py3-none-any.whl → 0.0.3.93__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.4
2
2
  Name: pytrilogy
3
- Version: 0.0.3.91
3
+ Version: 0.0.3.93
4
4
  Summary: Declarative, typed query language that compiles to SQL.
5
5
  Home-page:
6
6
  Author:
@@ -52,12 +52,12 @@ The Trilogy language is an experiment in better SQL for analytics - a streamline
52
52
  - Testability
53
53
  - Easy to use for humans and LLMs
54
54
 
55
- Trilogy is epsecially targeted at data consumption, providing a rich metadata layer that makes visualizing Trilogy easy and expressive.
55
+ Trilogy is especially powerful for data consumption, providing a rich metadata layer that makes creating, interperting, and visualizing queries easy and expressive.
56
56
 
57
57
  > [!TIP]
58
58
  > You can try Trilogy in a [open-source studio](https://trilogydata.dev/trilogy-studio-core/). More details on the language can be found on the [documentation](https://trilogydata.dev/).
59
59
 
60
- Start in the studio to explore Trilogy. For deeper work and integration, `pytrilogy` can be run locally to parse and execute trilogy model [.preql] files using the `trilogy` CLI tool, or can be run in python by importing the `trilogy` package.
60
+ We recommend starting with the studio to explore Trilogy. For integration, `pytrilogy` can be run locally to parse and execute trilogy model [.preql] files using the `trilogy` CLI tool, or can be run in python by importing the `trilogy` package.
61
61
 
62
62
  Installation: `pip install pytrilogy`
63
63
 
@@ -104,10 +104,13 @@ Save the following code in a file named `hello.preql`
104
104
 
105
105
  ```python
106
106
  # semantic model is abstract from data
107
+
108
+ type word string; # types can be used to provide expressive metadata tags that propagate through dataflow
109
+
107
110
  key sentence_id int;
108
- property sentence_id.word_one string; # comments after a definition
109
- property sentence_id.word_two string; # are syntactic sugar for adding
110
- property sentence_id.word_three string; # a description to it
111
+ property sentence_id.word_one string::word; # comments after a definition
112
+ property sentence_id.word_two string::word; # are syntactic sugar for adding
113
+ property sentence_id.word_three string::word; # a description to it
111
114
 
112
115
  # comments in other places are just comments
113
116
 
@@ -1,5 +1,5 @@
1
- pytrilogy-0.0.3.91.dist-info/licenses/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
2
- trilogy/__init__.py,sha256=LDWJhBs1R3AQp5R7V_F0oNbHt99WNfQUp0xPRujT930,303
1
+ pytrilogy-0.0.3.93.dist-info/licenses/LICENSE.md,sha256=5ZRvtTyCCFwz1THxDTjAu3Lidds9WjPvvzgVwPSYNDo,1042
2
+ trilogy/__init__.py,sha256=bRQB4EfrZ7rrA1l36UCSKRaXQEy-Rp6GDcsPvFJ-QFE,303
3
3
  trilogy/compiler.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  trilogy/constants.py,sha256=eKb_EJvSqjN9tGbdVEViwdtwwh8fZ3-jpOEDqL71y70,1691
5
5
  trilogy/engine.py,sha256=OK2RuqCIUId6yZ5hfF8J1nxGP0AJqHRZiafcowmW0xc,1728
@@ -12,24 +12,24 @@ trilogy/authoring/__init__.py,sha256=e74k-Jep4DLYPQU_2m0aVsYlw5HKTOucAKtdTbd6f2g
12
12
  trilogy/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  trilogy/core/constants.py,sha256=nizWYDCJQ1bigQMtkNIEMNTcN0NoEAXiIHLzpelxQ24,201
14
14
  trilogy/core/enums.py,sha256=RQRkpGHLtcBKAO6jZnmGVtSUnb00Q2rP56ltYGdfTok,8294
15
- trilogy/core/env_processor.py,sha256=pFsxnluKIusGKx1z7tTnfsd_xZcPy9pZDungkjkyvI0,3170
15
+ trilogy/core/env_processor.py,sha256=pD_YYuDG6CMybmwW9H2w958RloA7lEeVbzKXP6ltz2o,4078
16
16
  trilogy/core/environment_helpers.py,sha256=VvPIiFemqaLLpIpLIqprfu63K7muZ1YzNg7UZIUph8w,8267
17
17
  trilogy/core/ergonomics.py,sha256=e-7gE29vPLFdg0_A1smQ7eOrUwKl5VYdxRSTddHweRA,1631
18
18
  trilogy/core/exceptions.py,sha256=jYEduuMehcMkmCpf-OC_taELPZm7qNfeSNzIWkDYScs,707
19
19
  trilogy/core/functions.py,sha256=hnfcNjAD-XQ572vEwuUEAdBf8zKFWYwPeHIpESjUpZs,32928
20
- trilogy/core/graph_models.py,sha256=BYhJzHKSgnZHVLJs1CfsgrxTPHqKqPNeA64RlozGY0A,3498
20
+ trilogy/core/graph_models.py,sha256=zBzUwhYpnDJG91pWtk9ngw1WiTgHkMawyrqXptcGWGA,3847
21
21
  trilogy/core/internal.py,sha256=wFx4e1I0mtx159YFShSXeUBSQ82NINtAbOI-92RX4i8,2151
22
22
  trilogy/core/optimization.py,sha256=ojpn-p79lr03SSVQbbw74iPCyoYpDYBmj1dbZ3oXCjI,8860
23
23
  trilogy/core/query_processor.py,sha256=5aFgv-2LVM1Uku9cR_tFuTRDwyLnxc95bCMAHeFy2AY,20332
24
24
  trilogy/core/utility.py,sha256=3VC13uSQWcZNghgt7Ot0ZTeEmNqs__cx122abVq9qhM,410
25
25
  trilogy/core/models/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
26
- trilogy/core/models/author.py,sha256=wiKmEIouIVuzaYSM30PGon_FA9nowtDgnyLO-x5znBI,80180
27
- trilogy/core/models/build.py,sha256=CyrSo4xgU-uDKW3xUVYs5cTk3Z3Z2BMWdGQNHnHZOqU,66127
28
- trilogy/core/models/build_environment.py,sha256=s_C9xAHuD3yZ26T15pWVBvoqvlp2LdZ8yjsv2_HdXLk,5363
29
- trilogy/core/models/core.py,sha256=NOvonI4Ip4thpz5WoJZWbbBa44PFfpd2hXGx2Cbi4CE,12521
26
+ trilogy/core/models/author.py,sha256=31WLOzOJMBoISjiIAAkY6wnH1Om8PPA_dh9tu17_djs,81553
27
+ trilogy/core/models/build.py,sha256=Eew1P6mNNB7ywSb37sw4XBvJ-yyJK5vg99oIXgqgXAw,70529
28
+ trilogy/core/models/build_environment.py,sha256=mpx7MKGc60fnZLVdeLi2YSREy7eQbQYycCrP4zF-rHU,5258
29
+ trilogy/core/models/core.py,sha256=nnz3ZROlVT18uygEWqqbfbHmcJkm2UC3VVCrsri_-K0,12836
30
30
  trilogy/core/models/datasource.py,sha256=wogTevZ-9CyUW2a8gjzqMCieircxi-J5lkI7EOAZnck,9596
31
31
  trilogy/core/models/environment.py,sha256=0IHSCFf5e5b4LPQN3vmjumtfM1iD1tN4WMoUr0UqxZI,27855
32
- trilogy/core/models/execute.py,sha256=sVWhjwWull-T6pUJizhrYVGCWHY3eZivVN6KNlhcHig,41839
32
+ trilogy/core/models/execute.py,sha256=PLSwllj0RnViY_R5ZKjyuZIZJvfOYDJn_Af-tBthzGM,41842
33
33
  trilogy/core/optimizations/__init__.py,sha256=YH2-mGXZnVDnBcWVi8vTbrdw7Qs5TivG4h38rH3js_I,290
34
34
  trilogy/core/optimizations/base_optimization.py,sha256=gzDOKImoFn36k7XBD3ysEYDnbnb6vdVIztUfFQZsGnM,513
35
35
  trilogy/core/optimizations/inline_datasource.py,sha256=2sWNRpoRInnTgo9wExVT_r9RfLAQHI57reEV5cGHUcg,4329
@@ -47,13 +47,14 @@ trilogy/core/processing/node_generators/basic_node.py,sha256=TLZCv4WS196a-0g5xgK
47
47
  trilogy/core/processing/node_generators/common.py,sha256=PdysdroW9DUADP7f5Wv_GKPUyCTROZV1g3L45fawxi8,9443
48
48
  trilogy/core/processing/node_generators/constant_node.py,sha256=LfpDq2WrBRZ3tGsLxw77LuigKfhbteWWh9L8BGdMGwk,1146
49
49
  trilogy/core/processing/node_generators/filter_node.py,sha256=ArBsQJl-4fWBJWCE28CRQ7UT7ErnFfbcseoQQZrBodY,11220
50
- trilogy/core/processing/node_generators/group_node.py,sha256=1QJhRxsTklJ5xq8wHlAURZaN9gL9FPpeCa1OJ7IwXnY,6769
50
+ trilogy/core/processing/node_generators/group_node.py,sha256=8HJ1lkOvIXfX3xoS2IMbM_wCu_mT0J_hQ7xnTaxsVlo,6611
51
51
  trilogy/core/processing/node_generators/group_to_node.py,sha256=jKcNCDOY6fNblrdZwaRU0sbUSr9H0moQbAxrGgX6iGA,3832
52
52
  trilogy/core/processing/node_generators/multiselect_node.py,sha256=GWV5yLmKTe1yyPhN60RG1Rnrn4ktfn9lYYXi_FVU4UI,7061
53
- trilogy/core/processing/node_generators/node_merge_node.py,sha256=sUKS9bSTeYNCyF9jibrkac1_QkmxD-k4x35nQK1b9cM,18312
53
+ trilogy/core/processing/node_generators/node_merge_node.py,sha256=rb6ltJyhAUFairG6LZJ111Zm2uQhXWsSZfSERahUNGc,18258
54
54
  trilogy/core/processing/node_generators/recursive_node.py,sha256=l5zdh0dURKwmAy8kK4OpMtZfyUEQRk6N-PwSWIyBpSM,2468
55
55
  trilogy/core/processing/node_generators/rowset_node.py,sha256=5L5u6xz1In8EaHQdcYgR2si-tz9WB9YLXURo4AkUT9A,6630
56
56
  trilogy/core/processing/node_generators/select_merge_node.py,sha256=Cv2GwNiYSmwewjuK8T3JB3pbgrLZFPsB75DCP153BMA,22818
57
+ trilogy/core/processing/node_generators/select_merge_node_v2.py,sha256=1HQedwstFbk3xc7B09ElJ3mMoIKixtkcCqjDIBfsxck,25707
57
58
  trilogy/core/processing/node_generators/select_node.py,sha256=Ta1G39V94gjX_AgyZDz9OqnwLz4BjY3D6Drx9YpziMQ,3555
58
59
  trilogy/core/processing/node_generators/synonym_node.py,sha256=AnAsa_Wj50NJ_IK0HSgab_7klYmKVrv0WI1uUe-GvEY,3766
59
60
  trilogy/core/processing/node_generators/union_node.py,sha256=VNo6Oey4p8etU9xrOh2oTT2lIOTvY6PULUPRvVa2uxU,2877
@@ -94,13 +95,13 @@ trilogy/hooks/graph_hook.py,sha256=5BfR7Dt0bgEsCLgwjowgCsVkboGYfVJGOz8g9mqpnos,4
94
95
  trilogy/hooks/query_debugger.py,sha256=1npRjww94sPV5RRBBlLqMJRaFkH9vhEY6o828MeoEcw,5583
95
96
  trilogy/metadata/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
96
97
  trilogy/parsing/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
97
- trilogy/parsing/common.py,sha256=yV1AckK0h8u1OFeGQBTMu-wuW5m63c5CcZuPicsTH_w,30660
98
+ trilogy/parsing/common.py,sha256=550-L0444GUuBFdiDWkOg_DxnMXtcJFUMES2R5zlwik,31026
98
99
  trilogy/parsing/config.py,sha256=Z-DaefdKhPDmSXLgg5V4pebhSB0h590vI0_VtHnlukI,111
99
100
  trilogy/parsing/exceptions.py,sha256=Xwwsv2C9kSNv2q-HrrKC1f60JNHShXcCMzstTSEbiCw,154
100
101
  trilogy/parsing/helpers.py,sha256=AbpHGcgLb-kRsJGnwFEktk7uzpZOCcBY74-YBdrKVGs,1
101
- trilogy/parsing/parse_engine.py,sha256=fgqCtV6sf9HrkViEjf6XXdRpPf4hJ1gSyzLXZ9sLBHs,80148
102
+ trilogy/parsing/parse_engine.py,sha256=minAI04kKs5uZqRumafMCvC9lRwlcXCLmDigcNOF_7w,80639
102
103
  trilogy/parsing/render.py,sha256=HSNntD82GiiwHT-TWPLXAaIMWLYVV5B5zQEsgwrHIBE,19605
103
- trilogy/parsing/trilogy.lark,sha256=ySzMMLxyPjn74MjFHZxXPTW-jHW68KLPJpiszPvZaO0,15780
104
+ trilogy/parsing/trilogy.lark,sha256=e2YVSxqzRov08AydtDSA8aqSJU2M1eJaidMEkHCdsYE,15896
104
105
  trilogy/scripts/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
105
106
  trilogy/scripts/trilogy.py,sha256=1L0XrH4mVHRt1C9T1HnaDv2_kYEfbWTb5_-cBBke79w,3774
106
107
  trilogy/std/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -111,8 +112,8 @@ trilogy/std/money.preql,sha256=XWwvAV3WxBsHX9zfptoYRnBigcfYwrYtBHXTME0xJuQ,2082
111
112
  trilogy/std/net.preql,sha256=WZCuvH87_rZntZiuGJMmBDMVKkdhTtxeHOkrXNwJ1EE,416
112
113
  trilogy/std/ranking.preql,sha256=LDoZrYyz4g3xsII9XwXfmstZD-_92i1Eox1UqkBIfi8,83
113
114
  trilogy/std/report.preql,sha256=LbV-XlHdfw0jgnQ8pV7acG95xrd1-p65fVpiIc-S7W4,202
114
- pytrilogy-0.0.3.91.dist-info/METADATA,sha256=_W2oS79HhEjdCvg8ZApmI5siy5k513pXXCO541NVziQ,9589
115
- pytrilogy-0.0.3.91.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
116
- pytrilogy-0.0.3.91.dist-info/entry_points.txt,sha256=ewBPU2vLnVexZVnB-NrVj-p3E-4vukg83Zk8A55Wp2w,56
117
- pytrilogy-0.0.3.91.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
118
- pytrilogy-0.0.3.91.dist-info/RECORD,,
115
+ pytrilogy-0.0.3.93.dist-info/METADATA,sha256=Cyriy0EbhAniv3y6S2WNWQdYcNJK1iePuzmd2JnbW0U,9746
116
+ pytrilogy-0.0.3.93.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
117
+ pytrilogy-0.0.3.93.dist-info/entry_points.txt,sha256=ewBPU2vLnVexZVnB-NrVj-p3E-4vukg83Zk8A55Wp2w,56
118
+ pytrilogy-0.0.3.93.dist-info/top_level.txt,sha256=cAy__NW_eMAa_yT9UnUNlZLFfxcg6eimUAZ184cdNiE,8
119
+ pytrilogy-0.0.3.93.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.3.91"
7
+ __version__ = "0.0.3.93"
8
8
 
9
9
  __all__ = ["parse", "Executor", "Dialects", "Environment", "CONFIG"]
@@ -8,26 +8,34 @@ from trilogy.core.models.build_environment import BuildEnvironment
8
8
 
9
9
 
10
10
  def add_concept(
11
- concept: BuildConcept, g: ReferenceGraph, concept_mapping: dict[str, BuildConcept]
11
+ concept: BuildConcept,
12
+ g: ReferenceGraph,
13
+ concept_mapping: dict[str, BuildConcept],
14
+ default_concept_graph: dict[str, BuildConcept],
15
+ seen: set[str],
12
16
  ):
13
- g.add_node(concept)
17
+
14
18
  # if we have sources, recursively add them
15
19
  node_name = concept_to_node(concept)
20
+ if node_name in seen:
21
+ return
22
+ seen.add(node_name)
23
+ g.add_node(concept)
16
24
  if concept.concept_arguments:
17
25
  for source in concept.concept_arguments:
18
26
  if not isinstance(source, BuildConcept):
19
27
  raise ValueError(
20
28
  f"Invalid non-build concept {source} passed into graph generation from {concept}"
21
29
  )
22
- generic = source.with_default_grain()
23
- add_concept(generic, g, concept_mapping)
30
+ generic = get_default_grain_concept(source, default_concept_graph)
31
+ add_concept(generic, g, concept_mapping, default_concept_graph, seen)
24
32
 
25
33
  g.add_edge(generic, node_name)
26
34
  for ps_address in concept.pseudonyms:
27
35
  if ps_address not in concept_mapping:
28
36
  raise SyntaxError(f"Concept {concept} has invalid pseudonym {ps_address}")
29
37
  pseudonym = concept_mapping[ps_address]
30
- pseudonym = pseudonym.with_default_grain()
38
+ pseudonym = get_default_grain_concept(pseudonym, default_concept_graph)
31
39
  pseudonym_node = concept_to_node(pseudonym)
32
40
  if (pseudonym_node, node_name) in g.edges and (
33
41
  node_name,
@@ -38,16 +46,29 @@ def add_concept(
38
46
  continue
39
47
  g.add_edge(pseudonym_node, node_name, pseudonym=True)
40
48
  g.add_edge(node_name, pseudonym_node, pseudonym=True)
41
- add_concept(pseudonym, g, concept_mapping)
49
+ add_concept(pseudonym, g, concept_mapping, default_concept_graph, seen)
50
+
51
+
52
+ def get_default_grain_concept(
53
+ concept: BuildConcept, default_concept_graph: dict[str, BuildConcept]
54
+ ) -> BuildConcept:
55
+ """Get the default grain concept from the graph."""
56
+ if concept.address in default_concept_graph:
57
+ return default_concept_graph[concept.address]
58
+ default = concept.with_default_grain()
59
+ default_concept_graph[concept.address] = default
60
+ return default
42
61
 
43
62
 
44
63
  def generate_adhoc_graph(
45
64
  concepts: list[BuildConcept],
46
65
  datasources: list[BuildDatasource],
66
+ default_concept_graph: dict[str, BuildConcept],
47
67
  restrict_to_listed: bool = False,
48
68
  ) -> ReferenceGraph:
49
69
  g = ReferenceGraph()
50
70
  concept_mapping = {x.address: x for x in concepts}
71
+ seen: set[str] = set()
51
72
  for concept in concepts:
52
73
  if not isinstance(concept, BuildConcept):
53
74
  raise ValueError(f"Invalid non-build concept {concept}")
@@ -55,7 +76,7 @@ def generate_adhoc_graph(
55
76
  # add all parsed concepts
56
77
  for concept in concepts:
57
78
 
58
- add_concept(concept, g, concept_mapping)
79
+ add_concept(concept, g, concept_mapping, default_concept_graph, seen)
59
80
 
60
81
  for dataset in datasources:
61
82
  node = datasource_to_node(dataset)
@@ -69,7 +90,7 @@ def generate_adhoc_graph(
69
90
  # if there is a key on a table at a different grain
70
91
  # add an FK edge to the canonical source, if it exists
71
92
  # for example, order ID on order product table
72
- default = concept.with_default_grain()
93
+ default = get_default_grain_concept(concept, default_concept_graph)
73
94
  if concept != default:
74
95
  g.add_edge(concept, default)
75
96
  g.add_edge(default, concept)
@@ -79,9 +100,10 @@ def generate_adhoc_graph(
79
100
  def generate_graph(
80
101
  environment: BuildEnvironment,
81
102
  ) -> ReferenceGraph:
82
-
103
+ default_concept_graph: dict[str, BuildConcept] = {}
83
104
  return generate_adhoc_graph(
84
105
  list(environment.concepts.values())
85
106
  + list(environment.alias_origin_lookup.values()),
86
107
  list(environment.datasources.values()),
108
+ default_concept_graph=default_concept_graph,
87
109
  )
@@ -54,7 +54,7 @@ def prune_sources_for_conditions(
54
54
  def concept_to_node(input: BuildConcept) -> str:
55
55
  # if input.purpose == Purpose.METRIC:
56
56
  # return f"c~{input.namespace}.{input.name}@{input.grain}"
57
- return f"c~{input.address}@{input.grain.without_condition()}"
57
+ return f"c~{input.address}@{input.grain.str_no_condition}"
58
58
 
59
59
 
60
60
  def datasource_to_node(input: BuildDatasource) -> str:
@@ -72,11 +72,15 @@ class ReferenceGraph(nx.DiGraph):
72
72
  def add_node(self, node_for_adding, **attr):
73
73
  if isinstance(node_for_adding, BuildConcept):
74
74
  node_name = concept_to_node(node_for_adding)
75
+ # if node_name in self.nodes:
76
+ # return
75
77
  attr["type"] = "concept"
76
78
  attr["concept"] = node_for_adding
77
79
  attr["grain"] = node_for_adding.grain
78
80
  elif isinstance(node_for_adding, BuildDatasource):
79
81
  node_name = datasource_to_node(node_for_adding)
82
+ # if node_name in self.nodes:
83
+ # return
80
84
  attr["type"] = "datasource"
81
85
  attr["ds"] = node_for_adding
82
86
  attr["grain"] = node_for_adding.grain
@@ -91,7 +95,10 @@ class ReferenceGraph(nx.DiGraph):
91
95
  if u_of_edge not in self.nodes:
92
96
  self.add_node(orig)
93
97
  elif isinstance(u_of_edge, BuildDatasource):
98
+ orig = u_of_edge
94
99
  u_of_edge = datasource_to_node(u_of_edge)
100
+ if u_of_edge not in self.nodes:
101
+ self.add_node(orig)
95
102
 
96
103
  if isinstance(v_of_edge, BuildConcept):
97
104
  orig = v_of_edge
@@ -99,5 +106,8 @@ class ReferenceGraph(nx.DiGraph):
99
106
  if v_of_edge not in self.nodes:
100
107
  self.add_node(orig)
101
108
  elif isinstance(v_of_edge, BuildDatasource):
109
+ orig = v_of_edge
102
110
  v_of_edge = datasource_to_node(v_of_edge)
111
+ if v_of_edge not in self.nodes:
112
+ self.add_node(orig)
103
113
  super().add_edge(u_of_edge, v_of_edge, **attr)
@@ -460,6 +460,8 @@ class HavingClause(WhereClause):
460
460
  class Grain(Namespaced, BaseModel):
461
461
  components: set[str] = Field(default_factory=set)
462
462
  where_clause: Optional["WhereClause"] = None
463
+ _str: str | None = None
464
+ _abstract: bool = False
463
465
 
464
466
  def without_condition(self):
465
467
  return Grain(components=self.components)
@@ -484,12 +486,9 @@ class Grain(Namespaced, BaseModel):
484
486
  from trilogy.parsing.common import concepts_to_grain_concepts
485
487
 
486
488
  x = Grain.model_construct(
487
- components={
488
- c.address
489
- for c in concepts_to_grain_concepts(
490
- concepts, environment=environment, local_concepts=local_concepts
491
- )
492
- },
489
+ components=concepts_to_grain_concepts(
490
+ concepts, environment=environment, local_concepts=local_concepts
491
+ ),
493
492
  where_clause=where_clause,
494
493
  )
495
494
 
@@ -550,17 +549,22 @@ class Grain(Namespaced, BaseModel):
550
549
  where_clause=self.where_clause,
551
550
  )
552
551
 
553
- @property
554
- def abstract(self):
552
+ def _gen_abstract(self) -> bool:
555
553
  return not self.components or all(
556
554
  [c.endswith(ALL_ROWS_CONCEPT) for c in self.components]
557
555
  )
558
556
 
557
+ @property
558
+ def abstract(self):
559
+ if not self._abstract:
560
+ self._abstract = self._gen_abstract()
561
+ return self._abstract
562
+
559
563
  def __eq__(self, other: object):
560
564
  if isinstance(other, list):
561
- if not all([isinstance(c, Concept) for c in other]):
562
- return False
563
- return self.components == set([c.address for c in other])
565
+ if all([isinstance(c, Concept) for c in other]):
566
+ return self.components == set([c.address for c in other])
567
+ return False
564
568
  if not isinstance(other, Grain):
565
569
  return False
566
570
  if self.components == other.components:
@@ -581,15 +585,20 @@ class Grain(Namespaced, BaseModel):
581
585
  intersection = self.components.intersection(other.components)
582
586
  return Grain(components=intersection)
583
587
 
584
- def __str__(self):
588
+ def _gen_str(self) -> str:
585
589
  if self.abstract:
586
590
  base = "Grain<Abstract>"
587
591
  else:
588
- base = "Grain<" + ",".join([c for c in sorted(list(self.components))]) + ">"
592
+ base = "Grain<" + ",".join(sorted(self.components)) + ">"
589
593
  if self.where_clause:
590
594
  base += f"|{str(self.where_clause)}"
591
595
  return base
592
596
 
597
+ def __str__(self):
598
+ if not self._str:
599
+ self._str = self._gen_str()
600
+ return self._str
601
+
593
602
  def __radd__(self, other) -> "Grain":
594
603
  if other == 0:
595
604
  return self
@@ -1124,7 +1133,7 @@ class Concept(Addressable, DataTyped, ConceptArgs, Mergeable, Namespaced, BaseMo
1124
1133
  granularity=self.granularity,
1125
1134
  derivation=self.derivation,
1126
1135
  lineage=self.lineage,
1127
- grain=grain if grain else Grain(components=set()),
1136
+ grain=grain if grain else Grain.model_construct(components=set()),
1128
1137
  namespace=self.namespace,
1129
1138
  keys=self.keys,
1130
1139
  modifiers=self.modifiers,
@@ -2334,11 +2343,16 @@ class AlignItem(Namespaced, BaseModel):
2334
2343
 
2335
2344
  class CustomFunctionFactory:
2336
2345
  def __init__(
2337
- self, function: Expr, namespace: str, function_arguments: list[ArgBinding]
2346
+ self,
2347
+ function: Expr,
2348
+ namespace: str,
2349
+ function_arguments: list[ArgBinding],
2350
+ name: str,
2338
2351
  ):
2339
2352
  self.namespace = namespace
2340
2353
  self.function = function
2341
2354
  self.function_arguments = function_arguments
2355
+ self.name = name
2342
2356
 
2343
2357
  def with_namespace(self, namespace: str):
2344
2358
  self.namespace = namespace
@@ -2363,7 +2377,27 @@ class CustomFunctionFactory:
2363
2377
  for binding in self.function_arguments[len(creation_arg_list) :]:
2364
2378
  if binding.default is None:
2365
2379
  raise ValueError(f"Missing argument {binding.name}")
2380
+
2366
2381
  creation_arg_list.append(binding.default)
2382
+ for arg_idx, arg in enumerate(self.function_arguments):
2383
+ if not arg.datatype or arg.datatype == DataType.UNKNOWN:
2384
+ continue
2385
+ if arg_idx > len(creation_arg_list):
2386
+ continue
2387
+ comparison = arg_to_datatype(creation_arg_list[arg_idx])
2388
+ if comparison != arg.datatype:
2389
+ raise TypeError(
2390
+ f"Invalid type passed into custom function @{self.name} in position {arg_idx+1} for argument {arg.name}, expected {arg.datatype}, got {comparison}"
2391
+ )
2392
+ if isinstance(arg.datatype, TraitDataType):
2393
+ if not (
2394
+ isinstance(comparison, TraitDataType)
2395
+ and all(x in comparison.traits for x in arg.datatype.traits)
2396
+ ):
2397
+ raise TypeError(
2398
+ f"Invalid argument type passed into custom function @{self.name} in position {arg_idx+1} for argument {arg.name}, expected traits {arg.datatype.traits}, got {comparison}"
2399
+ )
2400
+
2367
2401
  if isinstance(nout, Mergeable):
2368
2402
  for idx, x in enumerate(creation_arg_list):
2369
2403
  if self.namespace == DEFAULT_NAMESPACE: