pytrilogy 0.3.149__cp313-cp313-win_amd64.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.
Files changed (207) hide show
  1. LICENSE.md +19 -0
  2. _preql_import_resolver/__init__.py +5 -0
  3. _preql_import_resolver/_preql_import_resolver.cp313-win_amd64.pyd +0 -0
  4. pytrilogy-0.3.149.dist-info/METADATA +555 -0
  5. pytrilogy-0.3.149.dist-info/RECORD +207 -0
  6. pytrilogy-0.3.149.dist-info/WHEEL +4 -0
  7. pytrilogy-0.3.149.dist-info/entry_points.txt +2 -0
  8. pytrilogy-0.3.149.dist-info/licenses/LICENSE.md +19 -0
  9. trilogy/__init__.py +27 -0
  10. trilogy/ai/README.md +10 -0
  11. trilogy/ai/__init__.py +19 -0
  12. trilogy/ai/constants.py +92 -0
  13. trilogy/ai/conversation.py +107 -0
  14. trilogy/ai/enums.py +7 -0
  15. trilogy/ai/execute.py +50 -0
  16. trilogy/ai/models.py +34 -0
  17. trilogy/ai/prompts.py +100 -0
  18. trilogy/ai/providers/__init__.py +0 -0
  19. trilogy/ai/providers/anthropic.py +106 -0
  20. trilogy/ai/providers/base.py +24 -0
  21. trilogy/ai/providers/google.py +146 -0
  22. trilogy/ai/providers/openai.py +89 -0
  23. trilogy/ai/providers/utils.py +68 -0
  24. trilogy/authoring/README.md +3 -0
  25. trilogy/authoring/__init__.py +148 -0
  26. trilogy/constants.py +119 -0
  27. trilogy/core/README.md +52 -0
  28. trilogy/core/__init__.py +0 -0
  29. trilogy/core/constants.py +6 -0
  30. trilogy/core/enums.py +454 -0
  31. trilogy/core/env_processor.py +239 -0
  32. trilogy/core/environment_helpers.py +320 -0
  33. trilogy/core/ergonomics.py +193 -0
  34. trilogy/core/exceptions.py +123 -0
  35. trilogy/core/functions.py +1240 -0
  36. trilogy/core/graph_models.py +142 -0
  37. trilogy/core/internal.py +85 -0
  38. trilogy/core/models/__init__.py +0 -0
  39. trilogy/core/models/author.py +2670 -0
  40. trilogy/core/models/build.py +2603 -0
  41. trilogy/core/models/build_environment.py +165 -0
  42. trilogy/core/models/core.py +506 -0
  43. trilogy/core/models/datasource.py +436 -0
  44. trilogy/core/models/environment.py +756 -0
  45. trilogy/core/models/execute.py +1213 -0
  46. trilogy/core/optimization.py +251 -0
  47. trilogy/core/optimizations/__init__.py +12 -0
  48. trilogy/core/optimizations/base_optimization.py +17 -0
  49. trilogy/core/optimizations/hide_unused_concept.py +47 -0
  50. trilogy/core/optimizations/inline_datasource.py +102 -0
  51. trilogy/core/optimizations/predicate_pushdown.py +245 -0
  52. trilogy/core/processing/README.md +94 -0
  53. trilogy/core/processing/READMEv2.md +121 -0
  54. trilogy/core/processing/VIRTUAL_UNNEST.md +30 -0
  55. trilogy/core/processing/__init__.py +0 -0
  56. trilogy/core/processing/concept_strategies_v3.py +508 -0
  57. trilogy/core/processing/constants.py +15 -0
  58. trilogy/core/processing/discovery_node_factory.py +451 -0
  59. trilogy/core/processing/discovery_utility.py +548 -0
  60. trilogy/core/processing/discovery_validation.py +167 -0
  61. trilogy/core/processing/graph_utils.py +43 -0
  62. trilogy/core/processing/node_generators/README.md +9 -0
  63. trilogy/core/processing/node_generators/__init__.py +31 -0
  64. trilogy/core/processing/node_generators/basic_node.py +160 -0
  65. trilogy/core/processing/node_generators/common.py +270 -0
  66. trilogy/core/processing/node_generators/constant_node.py +38 -0
  67. trilogy/core/processing/node_generators/filter_node.py +315 -0
  68. trilogy/core/processing/node_generators/group_node.py +213 -0
  69. trilogy/core/processing/node_generators/group_to_node.py +117 -0
  70. trilogy/core/processing/node_generators/multiselect_node.py +207 -0
  71. trilogy/core/processing/node_generators/node_merge_node.py +695 -0
  72. trilogy/core/processing/node_generators/recursive_node.py +88 -0
  73. trilogy/core/processing/node_generators/rowset_node.py +165 -0
  74. trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
  75. trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +261 -0
  76. trilogy/core/processing/node_generators/select_merge_node.py +846 -0
  77. trilogy/core/processing/node_generators/select_node.py +95 -0
  78. trilogy/core/processing/node_generators/synonym_node.py +98 -0
  79. trilogy/core/processing/node_generators/union_node.py +91 -0
  80. trilogy/core/processing/node_generators/unnest_node.py +182 -0
  81. trilogy/core/processing/node_generators/window_node.py +201 -0
  82. trilogy/core/processing/nodes/README.md +28 -0
  83. trilogy/core/processing/nodes/__init__.py +179 -0
  84. trilogy/core/processing/nodes/base_node.py +522 -0
  85. trilogy/core/processing/nodes/filter_node.py +75 -0
  86. trilogy/core/processing/nodes/group_node.py +194 -0
  87. trilogy/core/processing/nodes/merge_node.py +420 -0
  88. trilogy/core/processing/nodes/recursive_node.py +46 -0
  89. trilogy/core/processing/nodes/select_node_v2.py +242 -0
  90. trilogy/core/processing/nodes/union_node.py +53 -0
  91. trilogy/core/processing/nodes/unnest_node.py +62 -0
  92. trilogy/core/processing/nodes/window_node.py +56 -0
  93. trilogy/core/processing/utility.py +823 -0
  94. trilogy/core/query_processor.py +604 -0
  95. trilogy/core/statements/README.md +35 -0
  96. trilogy/core/statements/__init__.py +0 -0
  97. trilogy/core/statements/author.py +536 -0
  98. trilogy/core/statements/build.py +0 -0
  99. trilogy/core/statements/common.py +20 -0
  100. trilogy/core/statements/execute.py +155 -0
  101. trilogy/core/table_processor.py +66 -0
  102. trilogy/core/utility.py +8 -0
  103. trilogy/core/validation/README.md +46 -0
  104. trilogy/core/validation/__init__.py +0 -0
  105. trilogy/core/validation/common.py +161 -0
  106. trilogy/core/validation/concept.py +146 -0
  107. trilogy/core/validation/datasource.py +227 -0
  108. trilogy/core/validation/environment.py +73 -0
  109. trilogy/core/validation/fix.py +256 -0
  110. trilogy/dialect/__init__.py +32 -0
  111. trilogy/dialect/base.py +1432 -0
  112. trilogy/dialect/bigquery.py +314 -0
  113. trilogy/dialect/common.py +147 -0
  114. trilogy/dialect/config.py +159 -0
  115. trilogy/dialect/dataframe.py +50 -0
  116. trilogy/dialect/duckdb.py +397 -0
  117. trilogy/dialect/enums.py +151 -0
  118. trilogy/dialect/metadata.py +173 -0
  119. trilogy/dialect/mock.py +190 -0
  120. trilogy/dialect/postgres.py +117 -0
  121. trilogy/dialect/presto.py +110 -0
  122. trilogy/dialect/results.py +89 -0
  123. trilogy/dialect/snowflake.py +129 -0
  124. trilogy/dialect/sql_server.py +137 -0
  125. trilogy/engine.py +48 -0
  126. trilogy/execution/__init__.py +17 -0
  127. trilogy/execution/config.py +119 -0
  128. trilogy/execution/state/__init__.py +0 -0
  129. trilogy/execution/state/exceptions.py +26 -0
  130. trilogy/execution/state/file_state_store.py +0 -0
  131. trilogy/execution/state/sqllite_state_store.py +0 -0
  132. trilogy/execution/state/state_store.py +406 -0
  133. trilogy/executor.py +692 -0
  134. trilogy/hooks/__init__.py +4 -0
  135. trilogy/hooks/base_hook.py +40 -0
  136. trilogy/hooks/graph_hook.py +135 -0
  137. trilogy/hooks/query_debugger.py +166 -0
  138. trilogy/metadata/__init__.py +0 -0
  139. trilogy/parser.py +10 -0
  140. trilogy/parsing/README.md +21 -0
  141. trilogy/parsing/__init__.py +0 -0
  142. trilogy/parsing/common.py +1069 -0
  143. trilogy/parsing/config.py +5 -0
  144. trilogy/parsing/exceptions.py +8 -0
  145. trilogy/parsing/helpers.py +1 -0
  146. trilogy/parsing/parse_engine.py +2876 -0
  147. trilogy/parsing/render.py +775 -0
  148. trilogy/parsing/trilogy.lark +546 -0
  149. trilogy/py.typed +0 -0
  150. trilogy/render.py +45 -0
  151. trilogy/scripts/README.md +9 -0
  152. trilogy/scripts/__init__.py +0 -0
  153. trilogy/scripts/agent.py +41 -0
  154. trilogy/scripts/agent_info.py +306 -0
  155. trilogy/scripts/common.py +432 -0
  156. trilogy/scripts/dependency/Cargo.lock +617 -0
  157. trilogy/scripts/dependency/Cargo.toml +39 -0
  158. trilogy/scripts/dependency/README.md +131 -0
  159. trilogy/scripts/dependency/build.sh +25 -0
  160. trilogy/scripts/dependency/src/directory_resolver.rs +387 -0
  161. trilogy/scripts/dependency/src/lib.rs +16 -0
  162. trilogy/scripts/dependency/src/main.rs +770 -0
  163. trilogy/scripts/dependency/src/parser.rs +435 -0
  164. trilogy/scripts/dependency/src/preql.pest +208 -0
  165. trilogy/scripts/dependency/src/python_bindings.rs +311 -0
  166. trilogy/scripts/dependency/src/resolver.rs +716 -0
  167. trilogy/scripts/dependency/tests/base.preql +3 -0
  168. trilogy/scripts/dependency/tests/cli_integration.rs +377 -0
  169. trilogy/scripts/dependency/tests/customer.preql +6 -0
  170. trilogy/scripts/dependency/tests/main.preql +9 -0
  171. trilogy/scripts/dependency/tests/orders.preql +7 -0
  172. trilogy/scripts/dependency/tests/test_data/base.preql +9 -0
  173. trilogy/scripts/dependency/tests/test_data/consumer.preql +1 -0
  174. trilogy/scripts/dependency.py +323 -0
  175. trilogy/scripts/display.py +555 -0
  176. trilogy/scripts/environment.py +59 -0
  177. trilogy/scripts/fmt.py +32 -0
  178. trilogy/scripts/ingest.py +487 -0
  179. trilogy/scripts/ingest_helpers/__init__.py +1 -0
  180. trilogy/scripts/ingest_helpers/foreign_keys.py +123 -0
  181. trilogy/scripts/ingest_helpers/formatting.py +93 -0
  182. trilogy/scripts/ingest_helpers/typing.py +161 -0
  183. trilogy/scripts/init.py +105 -0
  184. trilogy/scripts/parallel_execution.py +762 -0
  185. trilogy/scripts/plan.py +189 -0
  186. trilogy/scripts/refresh.py +161 -0
  187. trilogy/scripts/run.py +79 -0
  188. trilogy/scripts/serve.py +202 -0
  189. trilogy/scripts/serve_helpers/__init__.py +41 -0
  190. trilogy/scripts/serve_helpers/file_discovery.py +142 -0
  191. trilogy/scripts/serve_helpers/index_generation.py +206 -0
  192. trilogy/scripts/serve_helpers/models.py +38 -0
  193. trilogy/scripts/single_execution.py +131 -0
  194. trilogy/scripts/testing.py +143 -0
  195. trilogy/scripts/trilogy.py +75 -0
  196. trilogy/std/__init__.py +0 -0
  197. trilogy/std/color.preql +3 -0
  198. trilogy/std/date.preql +13 -0
  199. trilogy/std/display.preql +18 -0
  200. trilogy/std/geography.preql +22 -0
  201. trilogy/std/metric.preql +15 -0
  202. trilogy/std/money.preql +67 -0
  203. trilogy/std/net.preql +14 -0
  204. trilogy/std/ranking.preql +7 -0
  205. trilogy/std/report.preql +5 -0
  206. trilogy/std/semantic.preql +6 -0
  207. trilogy/utility.py +34 -0
@@ -0,0 +1,2670 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ from abc import ABC
5
+ from datetime import date, datetime
6
+ from functools import cached_property
7
+ from typing import (
8
+ TYPE_CHECKING,
9
+ Any,
10
+ Iterable,
11
+ List,
12
+ Optional,
13
+ Self,
14
+ Sequence,
15
+ Set,
16
+ Tuple,
17
+ Type,
18
+ Union,
19
+ )
20
+
21
+ from pydantic import (
22
+ BaseModel,
23
+ ConfigDict,
24
+ Field,
25
+ ValidationInfo,
26
+ computed_field,
27
+ field_validator,
28
+ model_validator,
29
+ )
30
+
31
+ from trilogy.constants import DEFAULT_NAMESPACE, MagicConstants
32
+ from trilogy.core.constants import ALL_ROWS_CONCEPT
33
+ from trilogy.core.enums import (
34
+ BooleanOperator,
35
+ ComparisonOperator,
36
+ ConceptSource,
37
+ DatePart,
38
+ Derivation,
39
+ FunctionClass,
40
+ FunctionType,
41
+ Granularity,
42
+ InfiniteFunctionArgs,
43
+ Modifier,
44
+ Ordering,
45
+ Purpose,
46
+ WindowOrder,
47
+ WindowType,
48
+ )
49
+ from trilogy.core.models.core import (
50
+ Addressable,
51
+ ArrayType,
52
+ DataType,
53
+ DataTyped,
54
+ ListWrapper,
55
+ MapType,
56
+ MapWrapper,
57
+ NumericType,
58
+ StructType,
59
+ TraitDataType,
60
+ TupleWrapper,
61
+ arg_to_datatype,
62
+ is_compatible_datatype,
63
+ )
64
+ from trilogy.utility import unique
65
+
66
+ # TODO: refactor to avoid these
67
+ if TYPE_CHECKING:
68
+ from trilogy.core.models.environment import Environment
69
+
70
+
71
+ class Namespaced(ABC):
72
+ def with_namespace(self, namespace: str):
73
+ raise NotImplementedError
74
+
75
+
76
+ class Mergeable(ABC):
77
+ def with_merge(self, source: Concept, target: Concept, modifiers: List[Modifier]):
78
+ raise NotImplementedError
79
+
80
+ def with_reference_replacement(self, source: str, target: Expr | ArgBinding):
81
+ raise NotImplementedError(type(self))
82
+
83
+
84
+ class ConceptArgs(ABC):
85
+ @property
86
+ def concept_arguments(self) -> Sequence["ConceptRef"]:
87
+ raise NotImplementedError
88
+
89
+ @property
90
+ def existence_arguments(self) -> Sequence[tuple["ConceptRef", ...]]:
91
+ return []
92
+
93
+ @property
94
+ def row_arguments(self) -> Sequence["ConceptRef"]:
95
+ return self.concept_arguments
96
+
97
+
98
+ class HasUUID(ABC):
99
+ @property
100
+ def uuid(self) -> str:
101
+ return hashlib.md5(str(self).encode()).hexdigest()
102
+
103
+
104
+ def compute_safe_address(namespace: str, name: str) -> str:
105
+ if namespace == DEFAULT_NAMESPACE:
106
+ return name.replace(".", "_")
107
+ elif namespace:
108
+ return f"{namespace.replace('.', '_')}_{name.replace('.', '_')}"
109
+ return name.replace(".", "_")
110
+
111
+
112
+ class ConceptRef(Addressable, Namespaced, DataTyped, Mergeable, BaseModel):
113
+ address: str
114
+ datatype: (
115
+ DataType | TraitDataType | ArrayType | StructType | MapType | NumericType
116
+ ) = DataType.UNKNOWN
117
+ metadata: Optional["Metadata"] = None
118
+
119
+ @property
120
+ def reference(self):
121
+ return self
122
+
123
+ @property
124
+ def line_no(self) -> int | None:
125
+ if self.metadata:
126
+ return self.metadata.line_number
127
+ return None
128
+
129
+ def __repr__(self):
130
+ return f"ref:{self.address}"
131
+
132
+ def __str__(self):
133
+ return self.__repr__()
134
+
135
+ def __eq__(self, other):
136
+ if isinstance(other, Concept):
137
+ return self.address == other.address
138
+ elif isinstance(other, str):
139
+ return self.address == other
140
+ elif isinstance(other, ConceptRef):
141
+ return self.address == other.address
142
+ return False
143
+
144
+ @property
145
+ def namespace(self):
146
+ return self.address.rsplit(".", 1)[0]
147
+
148
+ @property
149
+ def name(self):
150
+ return self.address.rsplit(".", 1)[1]
151
+
152
+ @property
153
+ def safe_address(self) -> str:
154
+ return compute_safe_address(self.namespace, self.name)
155
+
156
+ @property
157
+ def output_datatype(self):
158
+ return self.datatype
159
+
160
+ def with_merge(
161
+ self, source: Concept, target: Concept, modifiers: List[Modifier]
162
+ ) -> ConceptRef:
163
+ if self.address == source.address:
164
+ return ConceptRef.model_construct(
165
+ address=target.address, datatype=target.datatype, metadata=self.metadata
166
+ )
167
+ return self
168
+
169
+ def with_namespace(self, namespace: str):
170
+ return ConceptRef.model_construct(
171
+ address=address_with_namespace(self.address, namespace),
172
+ datatype=self.datatype,
173
+ metadata=self.metadata,
174
+ )
175
+
176
+ def with_reference_replacement(self, source: str, target: Expr | ArgBinding):
177
+ # a reference might be to an attribute of a struct that is bound late
178
+ # if the replacement is a parent in the access path; replace reference
179
+ # with an attribute access call
180
+ candidates = [f"{DEFAULT_NAMESPACE}.{self.address}", self.address]
181
+ for candidate in candidates:
182
+ if candidate == source:
183
+ return target
184
+ if not candidate.startswith(f"{source}."):
185
+ continue
186
+ attribute = self.address.rsplit(".", 1)[1]
187
+ dtype = arg_to_datatype(target)
188
+ if not isinstance(dtype, StructType):
189
+ continue
190
+ output_type = dtype.field_types.get(attribute, DataType.UNKNOWN)
191
+ return Function(
192
+ arguments=[target, self.address.rsplit(".", 1)[1]],
193
+ operator=FunctionType.ATTR_ACCESS,
194
+ arg_count=2,
195
+ output_datatype=output_type,
196
+ output_purpose=Purpose.PROPERTY,
197
+ )
198
+ return self
199
+
200
+
201
+ class UndefinedConcept(ConceptRef):
202
+ pass
203
+
204
+ @property
205
+ def reference(self):
206
+ return self
207
+
208
+ @property
209
+ def purpose(self):
210
+ return Purpose.UNKNOWN
211
+
212
+
213
+ def address_with_namespace(address: str, namespace: str) -> str:
214
+ existing_ns = address.split(".", 1)[0]
215
+ if "." in address:
216
+ existing_name = address.split(".", 1)[1]
217
+ else:
218
+ existing_name = address
219
+ if existing_name == ALL_ROWS_CONCEPT:
220
+ return address
221
+ if existing_ns == DEFAULT_NAMESPACE:
222
+ return f"{namespace}.{existing_name}"
223
+ return f"{namespace}.{address}"
224
+
225
+
226
+ class Parenthetical(
227
+ DataTyped,
228
+ ConceptArgs,
229
+ Mergeable,
230
+ Namespaced,
231
+ BaseModel,
232
+ ):
233
+ content: "Expr"
234
+
235
+ @field_validator("content", mode="before")
236
+ @classmethod
237
+ def content_validator(cls, v, info: ValidationInfo):
238
+ if isinstance(v, Concept):
239
+ return v.reference
240
+ return v
241
+
242
+ def __add__(self, other) -> Union["Parenthetical", "Conditional"]:
243
+ if other is None:
244
+ return self
245
+ elif isinstance(other, (Comparison, Conditional, Parenthetical)):
246
+ return Conditional(left=self, right=other, operator=BooleanOperator.AND)
247
+ raise ValueError(f"Cannot add {self.__class__} and {type(other)}")
248
+
249
+ def __str__(self):
250
+ return self.__repr__()
251
+
252
+ def __repr__(self):
253
+ return f"({str(self.content)})"
254
+
255
+ def with_namespace(self, namespace: str) -> Parenthetical:
256
+ return Parenthetical.model_construct(
257
+ content=(
258
+ self.content.with_namespace(namespace)
259
+ if isinstance(self.content, Namespaced)
260
+ else self.content
261
+ )
262
+ )
263
+
264
+ def with_merge(self, source: Concept, target: Concept, modifiers: List[Modifier]):
265
+ return Parenthetical.model_construct(
266
+ content=(
267
+ self.content.with_merge(source, target, modifiers)
268
+ if isinstance(self.content, Mergeable)
269
+ else self.content
270
+ )
271
+ )
272
+
273
+ def with_reference_replacement(self, source, target):
274
+ return Parenthetical.model_construct(
275
+ content=(
276
+ self.content.with_reference_replacement(source, target)
277
+ if isinstance(self.content, Mergeable)
278
+ else self.content
279
+ )
280
+ )
281
+
282
+ @property
283
+ def concept_arguments(self) -> Sequence[ConceptRef]:
284
+ base: List[ConceptRef] = []
285
+ x = self.content
286
+ if isinstance(x, ConceptRef):
287
+ base += [x]
288
+ elif isinstance(x, ConceptArgs):
289
+ base += x.concept_arguments
290
+ return base
291
+
292
+ @property
293
+ def row_arguments(self) -> Sequence[ConceptRef]:
294
+ if isinstance(self.content, ConceptArgs):
295
+ return self.content.row_arguments
296
+ return self.concept_arguments
297
+
298
+ @property
299
+ def existence_arguments(self) -> Sequence[tuple["ConceptRef", ...]]:
300
+ if isinstance(self.content, ConceptArgs):
301
+ return self.content.existence_arguments
302
+ return []
303
+
304
+ @property
305
+ def output_datatype(self):
306
+ return arg_to_datatype(self.content)
307
+
308
+
309
+ class Conditional(Mergeable, ConceptArgs, Namespaced, DataTyped, BaseModel):
310
+ left: Expr
311
+ right: Expr
312
+ operator: BooleanOperator
313
+
314
+ @field_validator("left", mode="before")
315
+ @classmethod
316
+ def left_validator(cls, v, info: ValidationInfo):
317
+ if isinstance(v, Concept):
318
+ return v.reference
319
+ return v
320
+
321
+ @field_validator("right", mode="before")
322
+ @classmethod
323
+ def right_validator(cls, v, info: ValidationInfo):
324
+ if isinstance(v, Concept):
325
+ return v.reference
326
+ return v
327
+
328
+ def __add__(self, other) -> "Conditional":
329
+ if other is None:
330
+ return self
331
+ elif str(other) == str(self):
332
+ return self
333
+ elif isinstance(other, (Comparison, Conditional, Parenthetical)):
334
+ return Conditional.model_construct(
335
+ left=self, right=other, operator=BooleanOperator.AND
336
+ )
337
+ raise ValueError(f"Cannot add {self.__class__} and {type(other)}")
338
+
339
+ def __str__(self):
340
+ return self.__repr__()
341
+
342
+ def __repr__(self):
343
+ return f"{str(self.left)} {self.operator.value} {str(self.right)}"
344
+
345
+ def __eq__(self, other):
346
+ if not isinstance(other, Conditional):
347
+ return False
348
+ return (
349
+ self.left == other.left
350
+ and self.right == other.right
351
+ and self.operator == other.operator
352
+ )
353
+
354
+ def with_namespace(self, namespace: str) -> "Conditional":
355
+ return Conditional.model_construct(
356
+ left=(
357
+ self.left.with_namespace(namespace)
358
+ if isinstance(self.left, Namespaced)
359
+ else self.left
360
+ ),
361
+ right=(
362
+ self.right.with_namespace(namespace)
363
+ if isinstance(self.right, Namespaced)
364
+ else self.right
365
+ ),
366
+ operator=self.operator,
367
+ )
368
+
369
+ def with_merge(
370
+ self, source: Concept, target: Concept, modifiers: List[Modifier]
371
+ ) -> "Conditional":
372
+ return Conditional.model_construct(
373
+ left=(
374
+ self.left.with_merge(source, target, modifiers)
375
+ if isinstance(self.left, Mergeable)
376
+ else self.left
377
+ ),
378
+ right=(
379
+ self.right.with_merge(source, target, modifiers)
380
+ if isinstance(self.right, Mergeable)
381
+ else self.right
382
+ ),
383
+ operator=self.operator,
384
+ )
385
+
386
+ def with_reference_replacement(self, source, target):
387
+ return self.__class__.model_construct(
388
+ left=(
389
+ self.left.with_reference_replacement(source, target)
390
+ if isinstance(self.left, Mergeable)
391
+ else self.left
392
+ ),
393
+ right=(
394
+ self.right.with_reference_replacement(source, target)
395
+ if isinstance(self.right, Mergeable)
396
+ else self.right
397
+ ),
398
+ operator=self.operator,
399
+ )
400
+
401
+ @property
402
+ def concept_arguments(self) -> Sequence[ConceptRef]:
403
+ output = []
404
+ output += get_concept_arguments(self.left)
405
+ output += get_concept_arguments(self.right)
406
+ return output
407
+
408
+ @property
409
+ def row_arguments(self) -> Sequence[ConceptRef]:
410
+ output = []
411
+ output += get_concept_row_arguments(self.left)
412
+ output += get_concept_row_arguments(self.right)
413
+ return output
414
+
415
+ @property
416
+ def existence_arguments(self) -> Sequence[tuple[ConceptRef, ...]]:
417
+ output: list[tuple[ConceptRef, ...]] = []
418
+ if isinstance(self.left, ConceptArgs):
419
+ output += self.left.existence_arguments
420
+ if isinstance(self.right, ConceptArgs):
421
+ output += self.right.existence_arguments
422
+ return output
423
+
424
+ @property
425
+ def output_datatype(self):
426
+ # a conditional is always a boolean
427
+ return DataType.BOOL
428
+
429
+ def decompose(self):
430
+ chunks = []
431
+ if self.operator == BooleanOperator.AND:
432
+ for val in [self.left, self.right]:
433
+ if isinstance(val, Conditional):
434
+ chunks.extend(val.decompose())
435
+ else:
436
+ chunks.append(val)
437
+ else:
438
+ chunks.append(self)
439
+ return chunks
440
+
441
+
442
+ class WhereClause(Mergeable, ConceptArgs, Namespaced, BaseModel):
443
+ conditional: Union[SubselectComparison, Comparison, Conditional, Parenthetical]
444
+
445
+ def __repr__(self):
446
+ return str(self.conditional)
447
+
448
+ def __str__(self):
449
+ return self.__repr__()
450
+
451
+ @property
452
+ def concept_arguments(self) -> Sequence[ConceptRef]:
453
+ return self.conditional.concept_arguments
454
+
455
+ @property
456
+ def row_arguments(self) -> Sequence[ConceptRef]:
457
+ return self.conditional.row_arguments
458
+
459
+ @property
460
+ def existence_arguments(self) -> Sequence[tuple["ConceptRef", ...]]:
461
+ return self.conditional.existence_arguments
462
+
463
+ def with_merge(
464
+ self, source: Concept, target: Concept, modifiers: List[Modifier]
465
+ ) -> Self:
466
+ return self.__class__.model_construct(
467
+ conditional=self.conditional.with_merge(source, target, modifiers)
468
+ )
469
+
470
+ def with_namespace(self, namespace: str) -> Self:
471
+ return self.__class__.model_construct(
472
+ conditional=self.conditional.with_namespace(namespace)
473
+ )
474
+
475
+ def with_reference_replacement(self, source, target):
476
+ return self.__class__.model_construct(
477
+ conditional=self.conditional.with_reference_replacement(source, target)
478
+ )
479
+
480
+
481
+ class HavingClause(WhereClause):
482
+ pass
483
+
484
+
485
+ class Grain(Namespaced, BaseModel):
486
+ components: set[str] = Field(default_factory=set)
487
+ where_clause: Optional["WhereClause"] = None
488
+ _str: str | None = None
489
+ _abstract: bool = False
490
+
491
+ def without_condition(self):
492
+ return Grain(components=self.components)
493
+
494
+ def with_merge(self, source: Concept, target: Concept, modifiers: List[Modifier]):
495
+ new_components = set()
496
+ for c in self.components:
497
+ if c == source.address:
498
+ new_components.add(target.address)
499
+ else:
500
+ new_components.add(c)
501
+ return Grain.model_construct(components=new_components)
502
+
503
+ @classmethod
504
+ def from_concepts(
505
+ cls,
506
+ concepts: Iterable[Concept | ConceptRef | str],
507
+ environment: Environment | None = None,
508
+ where_clause: WhereClause | None = None,
509
+ local_concepts: dict[str, Concept] | None = None,
510
+ ) -> Grain:
511
+ from trilogy.parsing.common import concepts_to_grain_concepts
512
+
513
+ x = Grain.model_construct(
514
+ components=concepts_to_grain_concepts(
515
+ concepts, environment=environment, local_concepts=local_concepts
516
+ ),
517
+ where_clause=where_clause,
518
+ )
519
+
520
+ return x
521
+
522
+ def with_namespace(self, namespace: str) -> "Grain":
523
+ return Grain.model_construct(
524
+ components={address_with_namespace(c, namespace) for c in self.components},
525
+ where_clause=(
526
+ self.where_clause.with_namespace(namespace)
527
+ if self.where_clause
528
+ else None
529
+ ),
530
+ )
531
+
532
+ @field_validator("components", mode="before")
533
+ def component_validator(cls, v, info: ValidationInfo):
534
+ output = set()
535
+ if isinstance(v, list):
536
+ for vc in v:
537
+ if isinstance(vc, Addressable):
538
+ output.add(vc._address)
539
+ else:
540
+ output.add(vc)
541
+ else:
542
+ output = v
543
+ if not isinstance(output, set):
544
+ raise ValueError(f"Invalid grain component {output}, is not set")
545
+ if not all(isinstance(x, str) for x in output):
546
+ raise ValueError(f"Invalid component {output}")
547
+ return output
548
+
549
+ def __add__(self, other: "Grain") -> "Grain":
550
+ if not other:
551
+ return self
552
+ where = self.where_clause
553
+ if other.where_clause:
554
+ if not self.where_clause:
555
+ where = other.where_clause
556
+ elif not other.where_clause == self.where_clause:
557
+ where = WhereClause.model_construct(
558
+ conditional=Conditional(
559
+ left=self.where_clause.conditional,
560
+ right=other.where_clause.conditional,
561
+ operator=BooleanOperator.AND,
562
+ )
563
+ )
564
+ # raise NotImplementedError(
565
+ # f"Cannot merge grains with where clauses, self {self.where_clause} other {other.where_clause}"
566
+ # )
567
+ return Grain(
568
+ components=self.components.union(other.components), where_clause=where
569
+ )
570
+
571
+ def __sub__(self, other: "Grain") -> "Grain":
572
+ return Grain.model_construct(
573
+ components=self.components.difference(other.components),
574
+ where_clause=self.where_clause,
575
+ )
576
+
577
+ def _gen_abstract(self) -> bool:
578
+ return not self.components or all(
579
+ [c.endswith(ALL_ROWS_CONCEPT) for c in self.components]
580
+ )
581
+
582
+ @property
583
+ def abstract(self):
584
+ if not self._abstract:
585
+ self._abstract = self._gen_abstract()
586
+ return self._abstract
587
+
588
+ def __eq__(self, other: object):
589
+ if isinstance(other, list):
590
+ if all([isinstance(c, Concept) for c in other]):
591
+ return self.components == set([c.address for c in other])
592
+ return False
593
+ if not isinstance(other, Grain):
594
+ return False
595
+ if self.components == other.components:
596
+ return True
597
+ return False
598
+
599
+ def issubset(self, other: "Grain"):
600
+ return self.components.issubset(other.components)
601
+
602
+ def union(self, other: "Grain"):
603
+ addresses = self.components.union(other.components)
604
+ return Grain(components=addresses, where_clause=self.where_clause)
605
+
606
+ def isdisjoint(self, other: "Grain"):
607
+ return self.components.isdisjoint(other.components)
608
+
609
+ def intersection(self, other: "Grain") -> "Grain":
610
+ intersection = self.components.intersection(other.components)
611
+ return Grain(components=intersection)
612
+
613
+ def _gen_str(self) -> str:
614
+ if self.abstract:
615
+ base = "Grain<Abstract>"
616
+ else:
617
+ base = "Grain<" + ",".join(sorted(self.components)) + ">"
618
+ if self.where_clause:
619
+ base += f"|{str(self.where_clause)}"
620
+ return base
621
+
622
+ def __str__(self):
623
+ if not self._str:
624
+ self._str = self._gen_str()
625
+ return self._str
626
+
627
+ def __radd__(self, other) -> "Grain":
628
+ if other == 0:
629
+ return self
630
+ else:
631
+ return self.__add__(other)
632
+
633
+
634
+ class Comparison(ConceptArgs, Mergeable, DataTyped, Namespaced, BaseModel):
635
+ left: Union[
636
+ int,
637
+ str,
638
+ float,
639
+ list,
640
+ bool,
641
+ datetime,
642
+ date,
643
+ Function,
644
+ ConceptRef,
645
+ Conditional,
646
+ DataType,
647
+ Comparison,
648
+ FunctionCallWrapper,
649
+ Parenthetical,
650
+ MagicConstants,
651
+ WindowItem,
652
+ AggregateWrapper,
653
+ FilterItem,
654
+ ]
655
+ right: Union[
656
+ int,
657
+ str,
658
+ float,
659
+ list,
660
+ bool,
661
+ date,
662
+ datetime,
663
+ ConceptRef,
664
+ Function,
665
+ Conditional,
666
+ DataType,
667
+ Comparison,
668
+ FunctionCallWrapper,
669
+ Parenthetical,
670
+ MagicConstants,
671
+ WindowItem,
672
+ AggregateWrapper,
673
+ TupleWrapper,
674
+ FilterItem,
675
+ ]
676
+ operator: ComparisonOperator
677
+
678
+ @field_validator("left", mode="before")
679
+ @classmethod
680
+ def left_validator(cls, v, info: ValidationInfo):
681
+ if isinstance(v, Concept):
682
+ return v.reference
683
+ return v
684
+
685
+ @field_validator("right", mode="before")
686
+ @classmethod
687
+ def right_validator(cls, v, info: ValidationInfo):
688
+ if isinstance(v, Concept):
689
+ return v.reference
690
+ return v
691
+
692
+ @model_validator(mode="after")
693
+ def validate_comparison(self):
694
+ left_type = arg_to_datatype(self.left)
695
+ right_type = arg_to_datatype(self.right)
696
+ left_name = (
697
+ left_type.name if isinstance(left_type, DataType) else str(left_type)
698
+ )
699
+ right_name = (
700
+ right_type.name if isinstance(right_type, DataType) else str(right_type)
701
+ )
702
+ if self.operator in (ComparisonOperator.IS, ComparisonOperator.IS_NOT):
703
+ if self.right != MagicConstants.NULL and DataType.BOOL != right_type:
704
+ raise SyntaxError(
705
+ f"Cannot use {self.operator.value} with non-null or boolean value {self.right}"
706
+ )
707
+ elif self.operator in (ComparisonOperator.IN, ComparisonOperator.NOT_IN):
708
+
709
+ if isinstance(right_type, ArrayType) and not is_compatible_datatype(
710
+ left_type, right_type.value_data_type
711
+ ):
712
+ raise SyntaxError(
713
+ f"Cannot compare {left_type} and {right_type} with operator {self.operator} in {str(self)}"
714
+ )
715
+ elif isinstance(self.right, Concept) and not is_compatible_datatype(
716
+ left_type, right_type
717
+ ):
718
+ raise SyntaxError(
719
+ f"Cannot compare {left_name} and {right_name} with operator {self.operator} in {str(self)}"
720
+ )
721
+ else:
722
+ if not is_compatible_datatype(left_type, right_type):
723
+ raise SyntaxError(
724
+ f"Cannot compare {left_name} ({self.left}) and {right_name} ({self.right}) of different types with operator {self.operator.value} in {str(self)}"
725
+ )
726
+
727
+ return self
728
+
729
+ def __add__(self, other):
730
+ if other is None:
731
+ return self
732
+ if not isinstance(other, (Comparison, Conditional, Parenthetical)):
733
+ raise ValueError("Cannot add Comparison to non-Comparison")
734
+ if other == self:
735
+ return self
736
+ return Conditional(left=self, right=other, operator=BooleanOperator.AND)
737
+
738
+ def __repr__(self):
739
+ if isinstance(self.left, Concept):
740
+ left = self.left.address
741
+ else:
742
+ left = str(self.left)
743
+ if isinstance(self.right, Concept):
744
+ right = self.right.address
745
+ else:
746
+ right = str(self.right)
747
+ return f"{left} {self.operator.value} {right}"
748
+
749
+ def __str__(self):
750
+ return self.__repr__()
751
+
752
+ def __eq__(self, other):
753
+ if not isinstance(other, Comparison):
754
+ return False
755
+ return (
756
+ self.left == other.left
757
+ and self.right == other.right
758
+ and self.operator == other.operator
759
+ )
760
+
761
+ def with_merge(self, source: Concept, target: Concept, modifiers: List[Modifier]):
762
+ return self.__class__.model_construct(
763
+ left=(
764
+ self.left.with_merge(source, target, modifiers)
765
+ if isinstance(self.left, Mergeable)
766
+ else self.left
767
+ ),
768
+ right=(
769
+ self.right.with_merge(source, target, modifiers)
770
+ if isinstance(self.right, Mergeable)
771
+ else self.right
772
+ ),
773
+ operator=self.operator,
774
+ )
775
+
776
+ def with_reference_replacement(self, source, target):
777
+ return self.__class__.model_construct(
778
+ left=(
779
+ self.left.with_reference_replacement(source, target)
780
+ if isinstance(self.left, Mergeable)
781
+ else self.left
782
+ ),
783
+ right=(
784
+ self.right.with_reference_replacement(source, target)
785
+ if isinstance(self.right, Mergeable)
786
+ else self.right
787
+ ),
788
+ operator=self.operator,
789
+ )
790
+
791
+ def with_namespace(self, namespace: str):
792
+ return self.__class__.model_construct(
793
+ left=(
794
+ self.left.with_namespace(namespace)
795
+ if isinstance(self.left, Namespaced)
796
+ else self.left
797
+ ),
798
+ right=(
799
+ self.right.with_namespace(namespace)
800
+ if isinstance(self.right, Namespaced)
801
+ else self.right
802
+ ),
803
+ operator=self.operator,
804
+ )
805
+
806
+ @property
807
+ def concept_arguments(self) -> List[ConceptRef]:
808
+ """Return concepts directly referenced in where clause"""
809
+ output = []
810
+ output += get_concept_arguments(self.left)
811
+ output += get_concept_arguments(self.right)
812
+ return output
813
+
814
+ @property
815
+ def row_arguments(self) -> List[ConceptRef]:
816
+ output = []
817
+ output += get_concept_row_arguments(self.left)
818
+ output += get_concept_row_arguments(self.right)
819
+ return output
820
+
821
+ @property
822
+ def existence_arguments(self) -> List[Tuple[ConceptRef, ...]]:
823
+ """Return concepts directly referenced in where clause"""
824
+ output: List[Tuple[ConceptRef, ...]] = []
825
+ if isinstance(self.left, ConceptArgs):
826
+ output += self.left.existence_arguments
827
+ if isinstance(self.right, ConceptArgs):
828
+ output += self.right.existence_arguments
829
+ return output
830
+
831
+ @property
832
+ def output_datatype(self):
833
+ # a conditional is always a boolean
834
+ return DataType.BOOL
835
+
836
+
837
+ class SubselectComparison(Comparison):
838
+ def __eq__(self, other):
839
+ if not isinstance(other, SubselectComparison):
840
+ return False
841
+
842
+ comp = (
843
+ self.left == other.left
844
+ and self.right == other.right
845
+ and self.operator == other.operator
846
+ )
847
+ return comp
848
+
849
+ @property
850
+ def row_arguments(self) -> List[ConceptRef]:
851
+ return get_concept_row_arguments(self.left)
852
+
853
+ @property
854
+ def existence_arguments(self) -> list[tuple["ConceptRef", ...]]:
855
+ return [tuple(get_concept_arguments(self.right))]
856
+
857
+
858
+ class Concept(Addressable, DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
859
+ model_config = ConfigDict(
860
+ extra="forbid",
861
+ )
862
+ name: str
863
+ datatype: DataType | TraitDataType | ArrayType | StructType | MapType | NumericType
864
+ purpose: Purpose
865
+ derivation: Derivation = Derivation.ROOT
866
+ granularity: Granularity = Granularity.MULTI_ROW
867
+ metadata: Metadata = Field(
868
+ default_factory=lambda: Metadata(description=None, line_number=None),
869
+ validate_default=True,
870
+ )
871
+ lineage: Optional[
872
+ Union[
873
+ Function,
874
+ WindowItem,
875
+ FilterItem,
876
+ AggregateWrapper,
877
+ RowsetItem,
878
+ MultiSelectLineage,
879
+ Comparison,
880
+ ]
881
+ ] = None
882
+ namespace: str = Field(default=DEFAULT_NAMESPACE, validate_default=True)
883
+ keys: Optional[set[str]] = None
884
+ grain: "Grain" = Field(default=None, validate_default=True) # type: ignore
885
+ modifiers: List[Modifier] = Field(default_factory=list) # type: ignore
886
+ pseudonyms: set[str] = Field(default_factory=set)
887
+
888
+ def duplicate(self) -> Concept:
889
+ return self.model_copy(deep=True)
890
+
891
+ def __hash__(self):
892
+ return hash(
893
+ f"{self.name}+{self.datatype}+ {self.purpose} + {str(self.lineage)} + {self.namespace} + {str(self.grain)} + {str(self.keys)}"
894
+ )
895
+
896
+ def __repr__(self):
897
+ base = f"{self.address}@{self.grain}"
898
+ return base
899
+
900
+ @property
901
+ def is_internal(self) -> bool:
902
+ return self.namespace.startswith("_") or self.name.startswith("_")
903
+
904
+ @property
905
+ def reference(self) -> ConceptRef:
906
+ return ConceptRef.model_construct(
907
+ address=self.address,
908
+ datatype=self.output_datatype,
909
+ metadata=self.metadata,
910
+ )
911
+
912
+ @property
913
+ def output_datatype(self):
914
+ return self.datatype
915
+
916
+ @classmethod
917
+ def calculate_is_aggregate(cls, lineage):
918
+ if lineage and isinstance(lineage, Function):
919
+ if lineage.operator in FunctionClass.AGGREGATE_FUNCTIONS.value:
920
+ return True
921
+ if (
922
+ lineage
923
+ and isinstance(lineage, AggregateWrapper)
924
+ and lineage.function.operator in FunctionClass.AGGREGATE_FUNCTIONS.value
925
+ ):
926
+ return True
927
+ return False
928
+
929
+ @cached_property
930
+ def is_aggregate(self):
931
+ return self.calculate_is_aggregate(self.lineage)
932
+
933
+ def with_merge(self, source: Self, target: Self, modifiers: List[Modifier]) -> Self:
934
+ if self.address == source.address:
935
+ new = target.with_grain(self.grain.with_merge(source, target, modifiers))
936
+ new.pseudonyms.add(self.address)
937
+ return new
938
+ if not self.grain.components and not self.lineage and not self.keys:
939
+ return self
940
+ return self.__class__.model_construct(
941
+ name=self.name,
942
+ datatype=self.datatype,
943
+ purpose=self.purpose,
944
+ metadata=self.metadata,
945
+ derivation=self.derivation,
946
+ granularity=self.granularity,
947
+ lineage=(
948
+ self.lineage.with_merge(source, target, modifiers)
949
+ if self.lineage
950
+ else None
951
+ ),
952
+ grain=self.grain.with_merge(source, target, modifiers),
953
+ namespace=self.namespace,
954
+ keys=(
955
+ set(x if x != source.address else target.address for x in self.keys)
956
+ if self.keys
957
+ else None
958
+ ),
959
+ modifiers=self.modifiers,
960
+ pseudonyms=self.pseudonyms,
961
+ )
962
+
963
+ @field_validator("namespace", mode="plain")
964
+ @classmethod
965
+ def namespace_validation(cls, v):
966
+ return v or DEFAULT_NAMESPACE
967
+
968
+ @field_validator("metadata", mode="before")
969
+ @classmethod
970
+ def metadata_validation(cls, v):
971
+ v = v or Metadata()
972
+ return v
973
+
974
+ @field_validator("purpose", mode="after")
975
+ @classmethod
976
+ def purpose_validation(cls, v):
977
+ if v == Purpose.AUTO:
978
+ raise ValueError("Cannot set purpose to AUTO")
979
+ return v
980
+
981
+ @field_validator("grain", mode="before")
982
+ @classmethod
983
+ def parse_grain(cls, v, info: ValidationInfo) -> Grain:
984
+
985
+ # this is silly - rethink how we do grains
986
+ values = info.data
987
+
988
+ if not v and values.get("purpose", None) == Purpose.KEY:
989
+ v = Grain(
990
+ components={
991
+ f'{values.get("namespace", DEFAULT_NAMESPACE)}.{values["name"]}'
992
+ }
993
+ )
994
+ elif not v and values.get("purpose", None) == Purpose.PROPERTY:
995
+ v = Grain(components=values.get("keys", set()) or set())
996
+ elif (
997
+ "lineage" in values
998
+ and isinstance(values["lineage"], AggregateWrapper)
999
+ and values["lineage"].by
1000
+ ):
1001
+ v = Grain(components={c.address for c in values["lineage"].by})
1002
+ elif not v:
1003
+ v = Grain(components=set())
1004
+ elif isinstance(v, Grain):
1005
+ pass
1006
+ elif isinstance(v, Concept):
1007
+ v = Grain(components={v.address})
1008
+ elif isinstance(v, dict):
1009
+ v = Grain.model_validate(v)
1010
+ else:
1011
+ raise SyntaxError(f"Invalid grain {v} for concept {values['name']}")
1012
+ return v
1013
+
1014
+ def __eq__(self, other: object):
1015
+ if isinstance(other, str):
1016
+ if self.address == other:
1017
+ return True
1018
+ if isinstance(other, ConceptRef):
1019
+ return self.address == other.address
1020
+ if not isinstance(other, Concept):
1021
+ return False
1022
+ return (
1023
+ self.name == other.name
1024
+ and self.datatype == other.datatype
1025
+ and self.purpose == other.purpose
1026
+ and self.namespace == other.namespace
1027
+ and self.grain == other.grain
1028
+ and self.derivation == other.derivation
1029
+ and self.granularity == other.granularity
1030
+ # and self.keys == other.keys
1031
+ )
1032
+
1033
+ def __str__(self):
1034
+ grain = str(self.grain) if self.grain else "Grain<>"
1035
+ return f"{self.namespace}.{self.name}@{grain}"
1036
+
1037
+ @property
1038
+ def address(self) -> str:
1039
+ return f"{self.namespace}.{self.name}"
1040
+
1041
+ @property
1042
+ def output(self) -> "Concept":
1043
+ return self
1044
+
1045
+ @property
1046
+ def safe_address(self) -> str:
1047
+ return compute_safe_address(self.namespace, self.name)
1048
+
1049
+ def with_namespace(self, namespace: str) -> Self:
1050
+ return self.__class__.model_construct(
1051
+ name=self.name,
1052
+ datatype=self.datatype,
1053
+ purpose=self.purpose,
1054
+ granularity=self.granularity,
1055
+ derivation=self.derivation,
1056
+ metadata=self.metadata,
1057
+ lineage=self.lineage.with_namespace(namespace) if self.lineage else None,
1058
+ grain=(
1059
+ self.grain.with_namespace(namespace)
1060
+ if self.grain
1061
+ else Grain(components=set())
1062
+ ),
1063
+ namespace=(
1064
+ namespace + "." + self.namespace
1065
+ if self.namespace != DEFAULT_NAMESPACE
1066
+ else namespace
1067
+ ),
1068
+ keys=(
1069
+ set([address_with_namespace(x, namespace) for x in self.keys])
1070
+ if self.keys
1071
+ else None
1072
+ ),
1073
+ modifiers=self.modifiers,
1074
+ pseudonyms={address_with_namespace(v, namespace) for v in self.pseudonyms},
1075
+ )
1076
+
1077
+ def get_select_grain_and_keys(
1078
+ self, grain: Grain, environment: Environment
1079
+ ) -> Tuple[
1080
+ Function
1081
+ | WindowItem
1082
+ | FilterItem
1083
+ | AggregateWrapper
1084
+ | RowsetItem
1085
+ | MultiSelectLineage
1086
+ | Comparison
1087
+ | None,
1088
+ Grain,
1089
+ set[str] | None,
1090
+ ]:
1091
+ new_lineage = self.lineage
1092
+ final_grain = grain if not self.grain.components else self.grain
1093
+ keys = self.keys
1094
+ if not new_lineage:
1095
+ return new_lineage, final_grain, keys
1096
+
1097
+ if grain.components and isinstance(new_lineage, Function) and self.is_aggregate:
1098
+ grain_components: list[ConceptRef | Concept] = [
1099
+ environment.concepts[c].reference for c in grain.components
1100
+ ]
1101
+ new_lineage = AggregateWrapper.model_construct(
1102
+ function=new_lineage, by=grain_components
1103
+ )
1104
+ final_grain = grain
1105
+ keys = set(grain.components)
1106
+ elif isinstance(new_lineage, AggregateWrapper) and not new_lineage.by:
1107
+ grain_components = [
1108
+ environment.concepts[c].reference for c in grain.components
1109
+ ]
1110
+ new_lineage = AggregateWrapper.model_construct(
1111
+ function=new_lineage.function, by=grain_components
1112
+ )
1113
+ final_grain = grain
1114
+ keys = set([x.address for x in new_lineage.by])
1115
+ elif self.derivation == Derivation.BASIC:
1116
+
1117
+ pkeys: set[str] = set()
1118
+ assert new_lineage
1119
+ for x_ref in new_lineage.concept_arguments:
1120
+ x = environment.concepts[x_ref.address]
1121
+ if isinstance(x, UndefinedConcept):
1122
+ continue
1123
+ _, _, parent_keys = x.get_select_grain_and_keys(grain, environment)
1124
+ if parent_keys:
1125
+ pkeys.update(parent_keys)
1126
+ raw_keys = pkeys
1127
+ # deduplicate
1128
+
1129
+ final_grain = Grain.from_concepts(raw_keys, environment)
1130
+ keys = final_grain.components
1131
+ return new_lineage, final_grain, keys
1132
+
1133
+ def set_select_grain(self, grain: Grain, environment: Environment) -> Self:
1134
+ """Assign a mutable concept the appropriate grain/keys for a select"""
1135
+ new_lineage, final_grain, keys = self.get_select_grain_and_keys(
1136
+ grain, environment
1137
+ )
1138
+ return self.__class__.model_construct(
1139
+ name=self.name,
1140
+ datatype=self.datatype,
1141
+ purpose=self.purpose,
1142
+ granularity=self.granularity,
1143
+ derivation=self.derivation,
1144
+ metadata=self.metadata,
1145
+ lineage=new_lineage,
1146
+ grain=final_grain,
1147
+ namespace=self.namespace,
1148
+ keys=keys,
1149
+ modifiers=self.modifiers,
1150
+ pseudonyms=self.pseudonyms,
1151
+ )
1152
+
1153
+ def with_grain(self, grain: Optional["Grain"] = None) -> Self:
1154
+
1155
+ return self.__class__.model_construct(
1156
+ name=self.name,
1157
+ datatype=self.datatype,
1158
+ purpose=self.purpose,
1159
+ metadata=self.metadata,
1160
+ granularity=self.granularity,
1161
+ derivation=self.derivation,
1162
+ lineage=self.lineage,
1163
+ grain=grain if grain else Grain.model_construct(components=set()),
1164
+ namespace=self.namespace,
1165
+ keys=self.keys,
1166
+ modifiers=self.modifiers,
1167
+ pseudonyms=self.pseudonyms,
1168
+ )
1169
+
1170
+ @cached_property
1171
+ def sources(self) -> List["ConceptRef"]:
1172
+ if self.lineage:
1173
+ output: List[ConceptRef] = []
1174
+
1175
+ def get_sources(
1176
+ expr: Union[
1177
+ Function,
1178
+ WindowItem,
1179
+ FilterItem,
1180
+ AggregateWrapper,
1181
+ RowsetItem,
1182
+ MultiSelectLineage,
1183
+ Comparison,
1184
+ ],
1185
+ output: List[ConceptRef],
1186
+ ):
1187
+
1188
+ for item in expr.concept_arguments:
1189
+ if isinstance(item, (ConceptRef,)):
1190
+ if item.address == self.address:
1191
+ raise SyntaxError(
1192
+ f"Concept {self.address} references itself"
1193
+ )
1194
+ output.append(item)
1195
+
1196
+ # output += item.sources
1197
+
1198
+ get_sources(self.lineage, output)
1199
+ return output
1200
+ return []
1201
+
1202
+ @property
1203
+ def concept_arguments(self) -> List[ConceptRef]:
1204
+ return self.lineage.concept_arguments if self.lineage else []
1205
+
1206
+ @classmethod
1207
+ def calculate_derivation(self, lineage, purpose: Purpose) -> Derivation:
1208
+ from trilogy.core.models.build import (
1209
+ BuildAggregateWrapper,
1210
+ BuildComparison,
1211
+ BuildFilterItem,
1212
+ BuildFunction,
1213
+ BuildMultiSelectLineage,
1214
+ BuildRowsetItem,
1215
+ BuildWindowItem,
1216
+ )
1217
+
1218
+ if lineage and isinstance(lineage, (BuildWindowItem, WindowItem)):
1219
+ return Derivation.WINDOW
1220
+ elif lineage and isinstance(lineage, (BuildFilterItem, FilterItem)):
1221
+ return Derivation.FILTER
1222
+ elif lineage and isinstance(lineage, (BuildAggregateWrapper, AggregateWrapper)):
1223
+ return Derivation.AGGREGATE
1224
+ # elif lineage and isinstance(lineage, (BuildParenthetical, Parenthetical)):
1225
+ # return Derivation.PARENTHETICAL
1226
+ elif lineage and isinstance(lineage, (BuildRowsetItem, RowsetItem)):
1227
+ return Derivation.ROWSET
1228
+ elif lineage and isinstance(lineage, BuildComparison):
1229
+ return Derivation.BASIC
1230
+ elif lineage and isinstance(
1231
+ lineage, (BuildMultiSelectLineage, MultiSelectLineage)
1232
+ ):
1233
+ return Derivation.MULTISELECT
1234
+ elif (
1235
+ lineage
1236
+ and isinstance(lineage, (BuildFunction, Function))
1237
+ and lineage.operator in FunctionClass.AGGREGATE_FUNCTIONS.value
1238
+ ):
1239
+ return Derivation.AGGREGATE
1240
+ elif (
1241
+ lineage
1242
+ and isinstance(lineage, (BuildFunction, Function))
1243
+ and lineage.operator in FunctionClass.ONE_TO_MANY.value
1244
+ ):
1245
+ return Derivation.UNNEST
1246
+ elif (
1247
+ lineage
1248
+ and isinstance(lineage, (BuildFunction, Function))
1249
+ and lineage.operator == FunctionType.RECURSE_EDGE
1250
+ ):
1251
+ return Derivation.RECURSIVE
1252
+ elif (
1253
+ lineage
1254
+ and isinstance(lineage, (BuildFunction, Function))
1255
+ and lineage.operator == FunctionType.UNION
1256
+ ):
1257
+ return Derivation.UNION
1258
+ elif (
1259
+ lineage
1260
+ and isinstance(lineage, (BuildFunction, Function))
1261
+ and lineage.operator == FunctionType.GROUP
1262
+ ):
1263
+ return Derivation.GROUP_TO
1264
+ elif (
1265
+ lineage
1266
+ and isinstance(lineage, (BuildFunction, Function))
1267
+ and lineage.operator == FunctionType.ALIAS
1268
+ ):
1269
+ return Derivation.BASIC
1270
+ elif (
1271
+ lineage
1272
+ and isinstance(lineage, (BuildFunction, Function))
1273
+ and lineage.operator in FunctionClass.SINGLE_ROW.value
1274
+ ):
1275
+ return Derivation.CONSTANT
1276
+
1277
+ elif lineage and isinstance(lineage, (BuildFunction, Function)):
1278
+ if not lineage.concept_arguments:
1279
+ return Derivation.CONSTANT
1280
+ elif all(
1281
+ [x.derivation == Derivation.CONSTANT for x in lineage.concept_arguments]
1282
+ ):
1283
+ return Derivation.CONSTANT
1284
+ return Derivation.BASIC
1285
+ elif purpose == Purpose.CONSTANT:
1286
+ return Derivation.CONSTANT
1287
+ return Derivation.ROOT
1288
+
1289
+ @classmethod
1290
+ def calculate_granularity(cls, derivation: Derivation, grain: Grain, lineage):
1291
+ from trilogy.core.models.build import BuildFunction
1292
+
1293
+ if derivation == Derivation.CONSTANT:
1294
+ return Granularity.SINGLE_ROW
1295
+ elif derivation == Derivation.AGGREGATE:
1296
+ if all([x.endswith(ALL_ROWS_CONCEPT) for x in grain.components]):
1297
+ return Granularity.SINGLE_ROW
1298
+ elif (
1299
+ lineage
1300
+ and isinstance(lineage, (Function, BuildFunction))
1301
+ and lineage.operator
1302
+ in (FunctionType.UNNEST, FunctionType.UNION, FunctionType.DATE_SPINE)
1303
+ ):
1304
+ return Granularity.MULTI_ROW
1305
+ elif lineage and all(
1306
+ [x.granularity == Granularity.SINGLE_ROW for x in lineage.concept_arguments]
1307
+ ):
1308
+ return Granularity.SINGLE_ROW
1309
+ return Granularity.MULTI_ROW
1310
+
1311
+ # @property
1312
+ # def granularity(self) -> Granularity:
1313
+ # return self.calculate_granularity(self.derivation, self.grain, self.lineage)
1314
+
1315
+ def with_filter(
1316
+ self,
1317
+ condition: Conditional | Comparison | Parenthetical,
1318
+ environment: Environment | None = None,
1319
+ ) -> "Concept":
1320
+ from trilogy.utility import string_to_hash
1321
+
1322
+ if self.lineage and isinstance(self.lineage, FilterItem):
1323
+ if self.lineage.where.conditional == condition:
1324
+ return self
1325
+ hash = string_to_hash(self.name + str(condition))
1326
+ new_lineage = FilterItem(
1327
+ content=self.reference, where=WhereClause(conditional=condition)
1328
+ )
1329
+ new = Concept.model_construct(
1330
+ name=f"{self.name}_filter_{hash}",
1331
+ datatype=self.datatype,
1332
+ purpose=self.purpose,
1333
+ derivation=self.calculate_derivation(new_lineage, self.purpose),
1334
+ granularity=self.granularity,
1335
+ metadata=self.metadata,
1336
+ lineage=new_lineage,
1337
+ keys=(self.keys if self.purpose == Purpose.PROPERTY else None),
1338
+ grain=self.grain if self.grain else Grain(components=set()),
1339
+ namespace=self.namespace,
1340
+ modifiers=self.modifiers,
1341
+ pseudonyms=self.pseudonyms,
1342
+ )
1343
+ if environment:
1344
+ environment.add_concept(new)
1345
+ return new
1346
+
1347
+
1348
+ class UndefinedConceptFull(Concept, Mergeable, Namespaced):
1349
+ model_config = ConfigDict(arbitrary_types_allowed=True)
1350
+ name: str
1351
+ line_no: int | None = None
1352
+ datatype: (
1353
+ DataType | TraitDataType | ArrayType | StructType | MapType | NumericType
1354
+ ) = DataType.UNKNOWN
1355
+ purpose: Purpose = Purpose.UNKNOWN
1356
+
1357
+ @property
1358
+ def reference(self) -> UndefinedConcept:
1359
+ return UndefinedConcept(address=self.address)
1360
+
1361
+
1362
+ class OrderItem(Mergeable, ConceptArgs, Namespaced, BaseModel):
1363
+ # this needs to be a full concept as it may not exist in environment
1364
+ expr: Expr
1365
+ order: Ordering
1366
+
1367
+ @field_validator("expr", mode="before")
1368
+ def enforce_reference(cls, v):
1369
+ if isinstance(v, Concept):
1370
+ return v.reference
1371
+ return v
1372
+
1373
+ def with_namespace(self, namespace: str) -> "OrderItem":
1374
+ return OrderItem.model_construct(
1375
+ expr=(
1376
+ self.expr.with_namespace(namespace)
1377
+ if isinstance(self.expr, Namespaced)
1378
+ else self.expr
1379
+ ),
1380
+ order=self.order,
1381
+ )
1382
+
1383
+ def with_merge(
1384
+ self, source: Concept, target: Concept, modifiers: List[Modifier]
1385
+ ) -> "OrderItem":
1386
+ return OrderItem.model_construct(
1387
+ expr=(
1388
+ self.expr.with_merge(source, target, modifiers)
1389
+ if isinstance(self.expr, Mergeable)
1390
+ else self.expr
1391
+ ),
1392
+ order=self.order,
1393
+ )
1394
+
1395
+ def with_reference_replacement(self, source, target):
1396
+ return OrderItem.model_construct(
1397
+ expr=(
1398
+ self.expr.with_reference_replacement(source, target)
1399
+ if isinstance(self.expr, Mergeable)
1400
+ else self.expr
1401
+ ),
1402
+ order=self.order,
1403
+ )
1404
+
1405
+ @property
1406
+ def concept_arguments(self) -> Sequence[ConceptRef]:
1407
+ return get_concept_arguments(self.expr)
1408
+
1409
+ @property
1410
+ def row_arguments(self) -> Sequence[ConceptRef]:
1411
+ if isinstance(self.expr, ConceptArgs):
1412
+ return self.expr.row_arguments
1413
+ return self.concept_arguments
1414
+
1415
+ @property
1416
+ def existence_arguments(self) -> Sequence[tuple["ConceptRef", ...]]:
1417
+ if isinstance(self.expr, ConceptArgs):
1418
+ return self.expr.existence_arguments
1419
+ return []
1420
+
1421
+ @property
1422
+ def output_datatype(self):
1423
+ return arg_to_datatype(self.expr)
1424
+
1425
+
1426
+ class WindowItem(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
1427
+ type: WindowType
1428
+ content: FuncArgs
1429
+ order_by: List["OrderItem"]
1430
+ over: List["ConceptRef"] = Field(default_factory=list)
1431
+ index: Optional[int] = None
1432
+
1433
+ def __str__(self):
1434
+ return self.__repr__()
1435
+
1436
+ def __repr__(self):
1437
+ return f"{self.type.value} {self.content} by {self.index} over {self.over} order {self.order_by}"
1438
+
1439
+ @field_validator("content", mode="before")
1440
+ def enforce_concept_ref(cls, v):
1441
+ if isinstance(v, Concept):
1442
+ return ConceptRef(address=v.address, datatype=v.datatype)
1443
+ return v
1444
+
1445
+ @field_validator("over", mode="before")
1446
+ def enforce_concept_ref_over(cls, v):
1447
+ final = []
1448
+ for item in v:
1449
+ if isinstance(item, Concept):
1450
+ final.append(ConceptRef(address=item.address, datatype=item.datatype))
1451
+ else:
1452
+ final.append(item)
1453
+ return final
1454
+
1455
+ def with_merge(
1456
+ self, source: Concept, target: Concept, modifiers: List[Modifier]
1457
+ ) -> "WindowItem":
1458
+ output = WindowItem.model_construct(
1459
+ type=self.type,
1460
+ content=(
1461
+ self.content.with_merge(source, target, modifiers)
1462
+ if isinstance(self.content, Mergeable)
1463
+ else self.content
1464
+ ),
1465
+ over=[x.with_merge(source, target, modifiers) for x in self.over],
1466
+ order_by=[x.with_merge(source, target, modifiers) for x in self.order_by],
1467
+ index=self.index,
1468
+ )
1469
+ return output
1470
+
1471
+ def with_reference_replacement(self, source, target):
1472
+ return WindowItem.model_construct(
1473
+ type=self.type,
1474
+ content=self.content.with_reference_replacement(source, target),
1475
+ over=[x.with_reference_replacement(source, target) for x in self.over],
1476
+ order_by=[
1477
+ x.with_reference_replacement(source, target) for x in self.order_by
1478
+ ],
1479
+ index=self.index,
1480
+ )
1481
+
1482
+ def with_namespace(self, namespace: str) -> "WindowItem":
1483
+ return WindowItem.model_construct(
1484
+ type=self.type,
1485
+ content=(
1486
+ self.content.with_namespace(namespace)
1487
+ if isinstance(self.content, Namespaced)
1488
+ else self.content
1489
+ ),
1490
+ over=[x.with_namespace(namespace) for x in self.over],
1491
+ order_by=[x.with_namespace(namespace) for x in self.order_by],
1492
+ index=self.index,
1493
+ )
1494
+
1495
+ @property
1496
+ def concept_arguments(self) -> List[ConceptRef]:
1497
+ output = []
1498
+ output += get_concept_arguments(self.content)
1499
+ for order in self.order_by:
1500
+ output += get_concept_arguments(order)
1501
+ for item in self.over:
1502
+ output += get_concept_arguments(item)
1503
+ return output
1504
+
1505
+ @property
1506
+ def output_datatype(self):
1507
+ if self.type in (WindowType.RANK, WindowType.ROW_NUMBER):
1508
+ return DataType.INTEGER
1509
+ return self.content.output_datatype
1510
+
1511
+
1512
+ def get_basic_type(
1513
+ type: DataType | ArrayType | StructType | MapType | NumericType | TraitDataType,
1514
+ ) -> DataType:
1515
+ if isinstance(type, ArrayType):
1516
+ return DataType.ARRAY
1517
+ if isinstance(type, StructType):
1518
+ return DataType.STRUCT
1519
+ if isinstance(type, MapType):
1520
+ return DataType.MAP
1521
+ if isinstance(type, NumericType):
1522
+ return DataType.NUMERIC
1523
+ if isinstance(type, TraitDataType):
1524
+ return get_basic_type(type.type)
1525
+ return type
1526
+
1527
+
1528
+ class CaseWhen(Namespaced, DataTyped, ConceptArgs, Mergeable, BaseModel):
1529
+ comparison: Conditional | SubselectComparison | Comparison
1530
+ expr: "Expr"
1531
+
1532
+ @field_validator("expr", mode="before")
1533
+ def enforce_reference(cls, v):
1534
+ if isinstance(v, Concept):
1535
+ return v.reference
1536
+ return v
1537
+
1538
+ @property
1539
+ def output_datatype(self):
1540
+ return arg_to_datatype(self.expr)
1541
+
1542
+ def __str__(self):
1543
+ return self.__repr__()
1544
+
1545
+ def __repr__(self):
1546
+ return f"WHEN {str(self.comparison)} THEN {str(self.expr)}"
1547
+
1548
+ @property
1549
+ def concept_arguments(self):
1550
+ return get_concept_arguments(self.comparison) + get_concept_arguments(self.expr)
1551
+
1552
+ @property
1553
+ def concept_row_arguments(self):
1554
+ return get_concept_row_arguments(self.comparison) + get_concept_row_arguments(
1555
+ self.expr
1556
+ )
1557
+
1558
+ def with_namespace(self, namespace: str) -> CaseWhen:
1559
+ return CaseWhen.model_construct(
1560
+ comparison=self.comparison.with_namespace(namespace),
1561
+ expr=(
1562
+ self.expr.with_namespace(namespace)
1563
+ if isinstance(
1564
+ self.expr,
1565
+ Namespaced,
1566
+ )
1567
+ else self.expr
1568
+ ),
1569
+ )
1570
+
1571
+ def with_merge(
1572
+ self, source: Concept, target: Concept, modifiers: List[Modifier]
1573
+ ) -> CaseWhen:
1574
+ return CaseWhen.model_construct(
1575
+ comparison=self.comparison.with_merge(source, target, modifiers),
1576
+ expr=(
1577
+ self.expr.with_merge(source, target, modifiers)
1578
+ if isinstance(self.expr, Mergeable)
1579
+ else self.expr
1580
+ ),
1581
+ )
1582
+
1583
+ def with_reference_replacement(self, source, target):
1584
+ return CaseWhen.model_construct(
1585
+ comparison=self.comparison.with_reference_replacement(source, target),
1586
+ expr=(
1587
+ self.expr.with_reference_replacement(source, target)
1588
+ if isinstance(self.expr, Mergeable)
1589
+ else self.expr
1590
+ ),
1591
+ )
1592
+
1593
+
1594
+ class CaseElse(Namespaced, ConceptArgs, DataTyped, Mergeable, BaseModel):
1595
+ expr: "Expr"
1596
+ # this ensures that it's easily differentiable from CaseWhen
1597
+ discriminant: ComparisonOperator = ComparisonOperator.ELSE
1598
+
1599
+ def __str__(self):
1600
+ return self.__repr__()
1601
+
1602
+ def __repr__(self):
1603
+ return f"ELSE {str(self.expr)}"
1604
+
1605
+ @property
1606
+ def output_datatype(self):
1607
+ return arg_to_datatype(self.expr)
1608
+
1609
+ @field_validator("expr", mode="before")
1610
+ def enforce_expr(cls, v):
1611
+ if isinstance(v, Concept):
1612
+ return ConceptRef(address=v.address, datatype=v.datatype)
1613
+ return v
1614
+
1615
+ @property
1616
+ def concept_arguments(self):
1617
+ return get_concept_arguments(self.expr)
1618
+
1619
+ def with_merge(
1620
+ self, source: Concept, target: Concept, modifiers: List[Modifier]
1621
+ ) -> CaseElse:
1622
+ return CaseElse.model_construct(
1623
+ discriminant=self.discriminant,
1624
+ expr=(
1625
+ self.expr.with_merge(source, target, modifiers)
1626
+ if isinstance(self.expr, Mergeable)
1627
+ else self.expr
1628
+ ),
1629
+ )
1630
+
1631
+ def with_reference_replacement(self, source, target):
1632
+ return CaseElse.model_construct(
1633
+ discriminant=self.discriminant,
1634
+ expr=(
1635
+ self.expr.with_reference_replacement(
1636
+ source,
1637
+ target,
1638
+ )
1639
+ if isinstance(self.expr, Mergeable)
1640
+ else self.expr
1641
+ ),
1642
+ )
1643
+
1644
+ def with_namespace(self, namespace: str) -> CaseElse:
1645
+ return CaseElse.model_construct(
1646
+ discriminant=self.discriminant,
1647
+ expr=(
1648
+ self.expr.with_namespace(namespace)
1649
+ if isinstance(
1650
+ self.expr,
1651
+ Namespaced,
1652
+ )
1653
+ else self.expr
1654
+ ),
1655
+ )
1656
+
1657
+
1658
+ def get_concept_row_arguments(expr) -> List["ConceptRef"]:
1659
+ output = []
1660
+ if isinstance(expr, ConceptRef):
1661
+ output += [expr]
1662
+
1663
+ elif isinstance(expr, ConceptArgs):
1664
+ output += expr.row_arguments
1665
+ return output
1666
+
1667
+
1668
+ def get_concept_arguments(expr) -> List["ConceptRef"]:
1669
+ output = []
1670
+ if isinstance(expr, ConceptRef):
1671
+ output += [expr]
1672
+
1673
+ elif isinstance(
1674
+ expr,
1675
+ ConceptArgs,
1676
+ ):
1677
+ output += expr.concept_arguments
1678
+ return output
1679
+
1680
+
1681
+ def args_to_pretty(input: set[DataType]) -> str:
1682
+ return ", ".join(sorted([f"'{x.value}'" for x in input if x != DataType.UNKNOWN]))
1683
+
1684
+
1685
+ class Function(DataTyped, ConceptArgs, Mergeable, Namespaced, BaseModel):
1686
+ operator: FunctionType
1687
+ arg_count: int = Field(default=1)
1688
+ output_datatype: (
1689
+ DataType | ArrayType | StructType | MapType | NumericType | TraitDataType
1690
+ )
1691
+ output_purpose: Purpose
1692
+ valid_inputs: Optional[
1693
+ Union[
1694
+ Set[DataType],
1695
+ List[Set[DataType]],
1696
+ ]
1697
+ ] = None
1698
+ arguments: Sequence[FuncArgs]
1699
+
1700
+ class Config:
1701
+ frozen = True
1702
+
1703
+ def __repr__(self):
1704
+ return f'{self.operator.value}({",".join([str(a) for a in self.arguments])})'
1705
+
1706
+ def __str__(self):
1707
+ return self.__repr__()
1708
+
1709
+ @property
1710
+ def datatype(self):
1711
+ return self.output_datatype
1712
+
1713
+ @field_validator("arguments", mode="before")
1714
+ @classmethod
1715
+ def parse_arguments(cls, v, info: ValidationInfo):
1716
+ from trilogy.core.models.build import BuildConcept
1717
+ from trilogy.parsing.exceptions import ParseError
1718
+
1719
+ values = info.data
1720
+ arg_count = len(v)
1721
+ final = []
1722
+ for x in v:
1723
+ if isinstance(x, Concept) and not isinstance(x, BuildConcept):
1724
+ final.append(x.reference)
1725
+ else:
1726
+ final.append(x)
1727
+ v = final
1728
+ target_arg_count = values["arg_count"]
1729
+ operator_name = values["operator"].name
1730
+ # surface right error
1731
+ if "valid_inputs" not in values:
1732
+ return v
1733
+ valid_inputs = values["valid_inputs"]
1734
+ if not arg_count <= target_arg_count:
1735
+ if target_arg_count != InfiniteFunctionArgs:
1736
+ raise ParseError(
1737
+ f"Incorrect argument count to {operator_name} function, expects"
1738
+ f" {target_arg_count}, got {arg_count}"
1739
+ )
1740
+ # if all arguments can be any of the set type
1741
+ # turn this into an array for validation
1742
+ if isinstance(valid_inputs, set):
1743
+ valid_inputs = [valid_inputs for _ in v]
1744
+ elif not valid_inputs:
1745
+ return v
1746
+ for idx, arg in enumerate(v):
1747
+ if (
1748
+ isinstance(arg, ConceptRef)
1749
+ and get_basic_type(arg.datatype.data_type) not in valid_inputs[idx]
1750
+ ):
1751
+ if arg.datatype != DataType.UNKNOWN:
1752
+
1753
+ raise TypeError(
1754
+ f"Invalid argument type '{arg.datatype.data_type.value}' passed into {operator_name} function in position {idx+1}"
1755
+ f" from concept: {arg.name}. Valid: {args_to_pretty(valid_inputs[idx])}."
1756
+ )
1757
+ if (
1758
+ isinstance(arg, Function)
1759
+ and get_basic_type(arg.output_datatype) not in valid_inputs[idx]
1760
+ ):
1761
+ if arg.output_datatype != DataType.UNKNOWN:
1762
+ raise TypeError(
1763
+ f"Invalid argument type {arg.output_datatype}' passed into"
1764
+ f" {operator_name} function from function {arg.operator.name} in position {idx+1}. Valid: {args_to_pretty(valid_inputs[idx])}"
1765
+ )
1766
+ # check constants
1767
+ comparisons: List[Tuple[Type, DataType]] = [
1768
+ (str, DataType.STRING),
1769
+ (int, DataType.INTEGER),
1770
+ (float, DataType.FLOAT),
1771
+ (bool, DataType.BOOL),
1772
+ (DatePart, DataType.DATE_PART),
1773
+ ]
1774
+ for ptype, dtype in comparisons:
1775
+ if (
1776
+ isinstance(arg, ptype)
1777
+ and get_basic_type(dtype) in valid_inputs[idx]
1778
+ ):
1779
+ # attempt to exit early to avoid checking all types
1780
+ break
1781
+ elif isinstance(arg, ptype):
1782
+ if isinstance(arg, str) and DataType.DATE_PART in valid_inputs[idx]:
1783
+ if arg not in [x.value for x in DatePart]:
1784
+ pass
1785
+ else:
1786
+ break
1787
+ raise TypeError(
1788
+ f'Invalid {dtype} constant passed into {operator_name} "{arg}", expecting one of {valid_inputs[idx]}'
1789
+ )
1790
+ return v
1791
+
1792
+ def with_reference_replacement(self, source: str, target: Expr | ArgBinding):
1793
+ from trilogy.core.functions import arg_to_datatype, merge_datatypes
1794
+
1795
+ nargs = [
1796
+ (
1797
+ c.with_reference_replacement(
1798
+ source,
1799
+ target,
1800
+ )
1801
+ if isinstance(
1802
+ c,
1803
+ Mergeable,
1804
+ )
1805
+ else c
1806
+ )
1807
+ for c in self.arguments
1808
+ ]
1809
+ if self.output_datatype == DataType.UNKNOWN:
1810
+ new_output = merge_datatypes([arg_to_datatype(x) for x in nargs])
1811
+
1812
+ if self.operator == FunctionType.ATTR_ACCESS:
1813
+ if isinstance(new_output, StructType):
1814
+ new_output = new_output.field_types[str(nargs[1])]
1815
+ else:
1816
+ new_output = self.output_datatype
1817
+ # this is not ideal - see hacky logic for datatypes above
1818
+ # we need to figure out how to patch properly
1819
+ # should use function factory, but does not have environment access
1820
+ # probably move all datatype resolution to build?
1821
+ return Function.model_construct(
1822
+ operator=self.operator,
1823
+ arguments=nargs,
1824
+ output_datatype=new_output,
1825
+ output_purpose=self.output_purpose,
1826
+ valid_inputs=self.valid_inputs,
1827
+ arg_count=self.arg_count,
1828
+ )
1829
+
1830
+ def with_namespace(self, namespace: str) -> "Function":
1831
+ return Function.model_construct(
1832
+ operator=self.operator,
1833
+ arguments=[
1834
+ (
1835
+ c.with_namespace(namespace)
1836
+ if isinstance(
1837
+ c,
1838
+ Namespaced,
1839
+ )
1840
+ else c
1841
+ )
1842
+ for c in self.arguments
1843
+ ],
1844
+ output_datatype=self.output_datatype,
1845
+ output_purpose=self.output_purpose,
1846
+ valid_inputs=self.valid_inputs,
1847
+ arg_count=self.arg_count,
1848
+ )
1849
+
1850
+ def with_merge(
1851
+ self, source: Concept, target: Concept, modifiers: List[Modifier]
1852
+ ) -> "Function":
1853
+ return Function.model_construct(
1854
+ operator=self.operator,
1855
+ arguments=[
1856
+ (
1857
+ c.with_merge(source, target, modifiers)
1858
+ if isinstance(
1859
+ c,
1860
+ Mergeable,
1861
+ )
1862
+ else c
1863
+ )
1864
+ for c in self.arguments
1865
+ ],
1866
+ output_datatype=self.output_datatype,
1867
+ output_purpose=self.output_purpose,
1868
+ valid_inputs=self.valid_inputs,
1869
+ arg_count=self.arg_count,
1870
+ )
1871
+
1872
+ @property
1873
+ def concept_arguments(self) -> List[ConceptRef]:
1874
+ base = []
1875
+ for arg in self.arguments:
1876
+ base += get_concept_arguments(arg)
1877
+ return base
1878
+
1879
+
1880
+ class FunctionCallWrapper(
1881
+ DataTyped,
1882
+ ConceptArgs,
1883
+ Mergeable,
1884
+ Namespaced,
1885
+ BaseModel,
1886
+ ):
1887
+ content: Expr
1888
+ name: str
1889
+ args: List[Expr]
1890
+
1891
+ def __str__(self):
1892
+ return f'@{self.name}({",".join([str(x) for x in self.args])})'
1893
+
1894
+ def with_namespace(self, namespace) -> "FunctionCallWrapper":
1895
+ return FunctionCallWrapper.model_construct(
1896
+ content=(
1897
+ self.content.with_namespace(namespace)
1898
+ if isinstance(self.content, Namespaced)
1899
+ else self.content
1900
+ ),
1901
+ name=self.name,
1902
+ args=[
1903
+ x.with_namespace(namespace) if isinstance(x, Namespaced) else x
1904
+ for x in self.args
1905
+ ],
1906
+ )
1907
+
1908
+ def with_reference_replacement(self, source, target):
1909
+ raise NotImplementedError("Cannot reference replace")
1910
+ return self
1911
+
1912
+ def with_merge(
1913
+ self, source: Concept, target: Concept, modifiers: List[Modifier]
1914
+ ) -> "FunctionCallWrapper":
1915
+ return FunctionCallWrapper.model_construct(
1916
+ content=(
1917
+ self.content.with_merge(source, target, modifiers)
1918
+ if isinstance(self.content, Mergeable)
1919
+ else self.content
1920
+ ),
1921
+ name=self.name,
1922
+ args=[
1923
+ (
1924
+ x.with_merge(source, target, modifiers)
1925
+ if isinstance(x, Mergeable)
1926
+ else x
1927
+ )
1928
+ for x in self.args
1929
+ ],
1930
+ )
1931
+
1932
+ @property
1933
+ def concept_arguments(self) -> Sequence[ConceptRef]:
1934
+ base: List[ConceptRef] = []
1935
+ x = self.content
1936
+ if isinstance(x, ConceptRef):
1937
+ base += [x]
1938
+ elif isinstance(x, ConceptArgs):
1939
+ base += x.concept_arguments
1940
+ return base
1941
+
1942
+ @property
1943
+ def output_datatype(self):
1944
+ return arg_to_datatype(self.content)
1945
+
1946
+
1947
+ class AggregateWrapper(Mergeable, DataTyped, ConceptArgs, Namespaced, BaseModel):
1948
+ function: Function
1949
+ by: List[ConceptRef | Concept] = Field(default_factory=list)
1950
+
1951
+ @field_validator("by", mode="before")
1952
+ @classmethod
1953
+ def enforce_concept_ref(cls, v):
1954
+ output = []
1955
+ for item in v:
1956
+ if isinstance(item, Concept):
1957
+ output.append(item.reference)
1958
+ else:
1959
+ output.append(item)
1960
+ return output
1961
+
1962
+ def __str__(self):
1963
+ grain_str = [str(c) for c in self.by] if self.by else "abstract"
1964
+ return f"{str(self.function)}<{grain_str}>"
1965
+
1966
+ @property
1967
+ def datatype(self):
1968
+ return self.function.datatype
1969
+
1970
+ @property
1971
+ def concept_arguments(self) -> List[ConceptRef]:
1972
+ return self.function.concept_arguments + [x.reference for x in self.by]
1973
+
1974
+ @property
1975
+ def output_datatype(self):
1976
+ return self.function.output_datatype
1977
+
1978
+ @property
1979
+ def output_purpose(self):
1980
+ return self.function.output_purpose
1981
+
1982
+ def with_merge(self, source: Concept, target: Concept, modifiers: List[Modifier]):
1983
+ return AggregateWrapper.model_construct(
1984
+ function=self.function.with_merge(source, target, modifiers=modifiers),
1985
+ by=(
1986
+ [c.with_merge(source, target, modifiers) for c in self.by]
1987
+ if self.by
1988
+ else []
1989
+ ),
1990
+ )
1991
+
1992
+ def with_reference_replacement(self, source, target):
1993
+ return AggregateWrapper.model_construct(
1994
+ function=self.function.with_reference_replacement(source, target),
1995
+ by=(
1996
+ [c.with_reference_replacement(source, target) for c in self.by]
1997
+ if self.by
1998
+ else []
1999
+ ),
2000
+ )
2001
+
2002
+ def with_namespace(self, namespace: str) -> "AggregateWrapper":
2003
+ return AggregateWrapper.model_construct(
2004
+ function=self.function.with_namespace(namespace),
2005
+ by=[c.with_namespace(namespace) for c in self.by] if self.by else [],
2006
+ )
2007
+
2008
+
2009
+ class FilterItem(DataTyped, Namespaced, ConceptArgs, BaseModel):
2010
+ content: FuncArgs
2011
+ where: "WhereClause"
2012
+
2013
+ @field_validator("content", mode="before")
2014
+ def enforce_concept_ref(cls, v):
2015
+ if isinstance(v, Concept):
2016
+ return ConceptRef(address=v.address, datatype=v.datatype)
2017
+ return v
2018
+
2019
+ def __str__(self):
2020
+ return f"<Filter: {str(self.content)} where {str(self.where)}>"
2021
+
2022
+ def with_merge(
2023
+ self, source: Concept, target: Concept, modifiers: List[Modifier]
2024
+ ) -> "FilterItem":
2025
+ return FilterItem.model_construct(
2026
+ content=(
2027
+ self.content.with_merge(source, target, modifiers)
2028
+ if isinstance(self.content, Mergeable)
2029
+ else self.content
2030
+ ),
2031
+ where=self.where.with_merge(source, target, modifiers),
2032
+ )
2033
+
2034
+ def with_namespace(self, namespace: str) -> "FilterItem":
2035
+ return FilterItem.model_construct(
2036
+ content=(
2037
+ self.content.with_namespace(namespace)
2038
+ if isinstance(self.content, Namespaced)
2039
+ else self.content
2040
+ ),
2041
+ where=self.where.with_namespace(namespace),
2042
+ )
2043
+
2044
+ @property
2045
+ def output_datatype(self):
2046
+ return arg_to_datatype(self.content)
2047
+
2048
+ @property
2049
+ def concept_arguments(self):
2050
+ if isinstance(self.content, ConceptRef):
2051
+ return [self.content] + self.where.concept_arguments
2052
+ elif isinstance(self.content, ConceptArgs):
2053
+ return self.content.concept_arguments + self.where.concept_arguments
2054
+ return self.where.concept_arguments
2055
+
2056
+
2057
+ class RowsetLineage(Namespaced, Mergeable, BaseModel):
2058
+ name: str
2059
+ derived_concepts: List[ConceptRef]
2060
+ select: SelectLineage | MultiSelectLineage
2061
+
2062
+ def with_namespace(self, namespace: str):
2063
+ return RowsetLineage.model_construct(
2064
+ name=self.name,
2065
+ derived_concepts=[
2066
+ x.with_namespace(namespace) for x in self.derived_concepts
2067
+ ],
2068
+ select=self.select.with_namespace(namespace),
2069
+ )
2070
+
2071
+ def with_merge(self, source: Concept, target: Concept, modifiers: List[Modifier]):
2072
+ return RowsetLineage.model_construct(
2073
+ name=self.name,
2074
+ derived_concepts=[
2075
+ x.with_merge(source, target, modifiers) for x in self.derived_concepts
2076
+ ],
2077
+ select=self.select.with_merge(source, target, modifiers),
2078
+ )
2079
+
2080
+
2081
+ class RowsetItem(Mergeable, DataTyped, ConceptArgs, Namespaced, BaseModel):
2082
+ content: ConceptRef
2083
+ rowset: RowsetLineage
2084
+
2085
+ def __repr__(self):
2086
+ return f"<Rowset<{self.rowset.name}>: {str(self.content)}>"
2087
+
2088
+ def __str__(self):
2089
+ return self.__repr__()
2090
+
2091
+ def with_merge(self, source: Concept, target: Concept, modifiers: List[Modifier]):
2092
+ return RowsetItem.model_construct(
2093
+ content=self.content.with_merge(source, target, modifiers),
2094
+ rowset=self.rowset,
2095
+ )
2096
+
2097
+ def with_namespace(self, namespace: str) -> "RowsetItem":
2098
+ return RowsetItem.model_construct(
2099
+ content=self.content.with_namespace(namespace),
2100
+ rowset=self.rowset.with_namespace(namespace),
2101
+ )
2102
+
2103
+ @property
2104
+ def output(self) -> ConceptRef:
2105
+ return self.content
2106
+
2107
+ @property
2108
+ def output_datatype(self):
2109
+ return self.content.datatype
2110
+
2111
+ @property
2112
+ def concept_arguments(self):
2113
+ return [self.content]
2114
+
2115
+
2116
+ class OrderBy(Mergeable, Namespaced, BaseModel):
2117
+ items: List[OrderItem]
2118
+
2119
+ def with_namespace(self, namespace: str) -> "OrderBy":
2120
+ return OrderBy.model_construct(
2121
+ items=[x.with_namespace(namespace) for x in self.items]
2122
+ )
2123
+
2124
+ def with_merge(
2125
+ self, source: Concept, target: Concept, modifiers: List[Modifier]
2126
+ ) -> "OrderBy":
2127
+ return OrderBy.model_construct(
2128
+ items=[x.with_merge(source, target, modifiers) for x in self.items]
2129
+ )
2130
+
2131
+ @property
2132
+ def concept_arguments(self):
2133
+ base = []
2134
+ for x in self.items:
2135
+ base += x.concept_arguments
2136
+ return base
2137
+
2138
+
2139
+ class AlignClause(Namespaced, BaseModel):
2140
+ items: List[AlignItem]
2141
+
2142
+ def with_namespace(self, namespace: str) -> "AlignClause":
2143
+ return AlignClause.model_construct(
2144
+ items=[x.with_namespace(namespace) for x in self.items]
2145
+ )
2146
+
2147
+
2148
+ class DeriveItem(Namespaced, DataTyped, ConceptArgs, Mergeable, BaseModel):
2149
+ expr: Expr
2150
+ name: str
2151
+ namespace: str
2152
+
2153
+ @property
2154
+ def derived_concept(self) -> str:
2155
+ return f"{self.namespace}.{self.name}"
2156
+ # return ConceptRef(
2157
+ # address=f"{self.namespace}.{self.name}",
2158
+ # datatype=arg_to_datatype(self.expr),
2159
+ # )
2160
+
2161
+ def with_namespace(self, namespace):
2162
+ return DeriveItem.model_construct(
2163
+ expr=(self.expr.with_namespace(namespace) if self.expr else None),
2164
+ name=self.name,
2165
+ namespace=namespace,
2166
+ )
2167
+
2168
+ def with_merge(
2169
+ self, source: Concept, target: Concept, modifiers: List[Modifier]
2170
+ ) -> "DeriveItem":
2171
+ return DeriveItem.model_construct(
2172
+ expr=(
2173
+ self.expr.with_merge(source, target, modifiers)
2174
+ if isinstance(self.expr, Mergeable)
2175
+ else self.expr
2176
+ ),
2177
+ name=self.name,
2178
+ namespace=self.namespace,
2179
+ )
2180
+
2181
+ def with_reference_replacement(self, source, target):
2182
+ return DeriveItem.model_construct(
2183
+ expr=(
2184
+ self.expr.with_reference_replacement(source, target)
2185
+ if isinstance(self.expr, Mergeable)
2186
+ else self.expr
2187
+ ),
2188
+ name=self.name,
2189
+ namespace=self.namespace,
2190
+ )
2191
+
2192
+
2193
+ class DeriveClause(Mergeable, Namespaced, BaseModel):
2194
+ items: List[DeriveItem]
2195
+
2196
+ def with_namespace(self, namespace: str) -> "DeriveClause":
2197
+ return DeriveClause.model_construct(
2198
+ items=[
2199
+ x.with_namespace(namespace) if isinstance(x, Namespaced) else x
2200
+ for x in self.items
2201
+ ]
2202
+ )
2203
+
2204
+ def with_merge(
2205
+ self, source: Concept, target: Concept, modifiers: List[Modifier]
2206
+ ) -> "DeriveClause":
2207
+ return DeriveClause.model_construct(
2208
+ items=[
2209
+ (
2210
+ x.with_merge(source, target, modifiers)
2211
+ if isinstance(x, Mergeable)
2212
+ else x
2213
+ )
2214
+ for x in self.items
2215
+ ]
2216
+ )
2217
+
2218
+ def with_reference_replacement(self, source, target):
2219
+ return DeriveClause.model_construct(
2220
+ items=[
2221
+ (
2222
+ x.with_reference_replacement(source, target)
2223
+ if isinstance(x, Mergeable)
2224
+ else x
2225
+ )
2226
+ for x in self.items
2227
+ ]
2228
+ )
2229
+
2230
+
2231
+ class SelectLineage(Mergeable, Namespaced, BaseModel):
2232
+ selection: List[ConceptRef]
2233
+ hidden_components: set[str]
2234
+ local_concepts: dict[str, Concept]
2235
+ order_by: Optional[OrderBy] = None
2236
+ limit: Optional[int] = None
2237
+ meta: Metadata = Field(default_factory=lambda: Metadata())
2238
+ grain: Grain = Field(default_factory=Grain)
2239
+ where_clause: Union["WhereClause", None] = Field(default=None)
2240
+ having_clause: Union["HavingClause", None] = Field(default=None)
2241
+
2242
+ @property
2243
+ def output_components(self) -> List[ConceptRef]:
2244
+ return self.selection
2245
+
2246
+ def with_merge(
2247
+ self, source: Concept, target: Concept, modifiers: List[Modifier]
2248
+ ) -> SelectLineage:
2249
+ return SelectLineage.model_construct(
2250
+ selection=[x.with_merge(source, target, modifiers) for x in self.selection],
2251
+ hidden_components=self.hidden_components,
2252
+ local_concepts={
2253
+ x: y.with_merge(source, target, modifiers)
2254
+ for x, y in self.local_concepts.items()
2255
+ },
2256
+ order_by=(
2257
+ self.order_by.with_merge(source, target, modifiers)
2258
+ if self.order_by
2259
+ else None
2260
+ ),
2261
+ limit=self.limit,
2262
+ grain=self.grain.with_merge(source, target, modifiers),
2263
+ where_clause=(
2264
+ self.where_clause.with_merge(source, target, modifiers)
2265
+ if self.where_clause
2266
+ else None
2267
+ ),
2268
+ having_clause=(
2269
+ self.having_clause.with_merge(source, target, modifiers)
2270
+ if self.having_clause
2271
+ else None
2272
+ ),
2273
+ )
2274
+
2275
+ def with_namespace(self, namespace):
2276
+ return SelectLineage.model_construct(
2277
+ selection=[x.with_namespace(namespace) for x in self.selection],
2278
+ hidden_components=self.hidden_components,
2279
+ local_concepts={
2280
+ x: y.with_namespace(namespace) for x, y in self.local_concepts.items()
2281
+ },
2282
+ order_by=self.order_by.with_namespace(namespace) if self.order_by else None,
2283
+ limit=self.limit,
2284
+ meta=self.meta,
2285
+ grain=self.grain.with_namespace(namespace),
2286
+ where_clause=(
2287
+ self.where_clause.with_namespace(namespace)
2288
+ if self.where_clause
2289
+ else None
2290
+ ),
2291
+ having_clause=(
2292
+ self.having_clause.with_namespace(namespace)
2293
+ if self.having_clause
2294
+ else None
2295
+ ),
2296
+ )
2297
+
2298
+
2299
+ class MultiSelectLineage(Mergeable, ConceptArgs, Namespaced, BaseModel):
2300
+ selects: List[SelectLineage]
2301
+ align: AlignClause
2302
+
2303
+ namespace: str
2304
+ order_by: Optional[OrderBy] = None
2305
+ limit: Optional[int] = None
2306
+ where_clause: Union["WhereClause", None] = Field(default=None)
2307
+ having_clause: Union["HavingClause", None] = Field(default=None)
2308
+ derive: DeriveClause | None = None
2309
+ hidden_components: set[str]
2310
+
2311
+ @property
2312
+ def grain(self):
2313
+ base = Grain()
2314
+ for select in self.selects:
2315
+ base += select.grain
2316
+ return base
2317
+
2318
+ @property
2319
+ def output_components(self) -> list[ConceptRef]:
2320
+ output = [
2321
+ ConceptRef.model_construct(address=x, datatype=DataType.UNKNOWN)
2322
+ for x in self.derived_concepts
2323
+ ]
2324
+ for select in self.selects:
2325
+ output += select.output_components
2326
+ return [x for x in output if x.address not in self.hidden_components]
2327
+
2328
+ def with_merge(
2329
+ self, source: Concept, target: Concept, modifiers: List[Modifier]
2330
+ ) -> MultiSelectLineage:
2331
+ new = MultiSelectLineage.model_construct(
2332
+ selects=[s.with_merge(source, target, modifiers) for s in self.selects],
2333
+ align=self.align,
2334
+ derive=(
2335
+ self.derive.with_merge(source, target, modifiers)
2336
+ if self.derive
2337
+ else None
2338
+ ),
2339
+ namespace=self.namespace,
2340
+ hidden_components=self.hidden_components,
2341
+ order_by=(
2342
+ self.order_by.with_merge(source, target, modifiers)
2343
+ if self.order_by
2344
+ else None
2345
+ ),
2346
+ limit=self.limit,
2347
+ where_clause=(
2348
+ self.where_clause.with_merge(source, target, modifiers)
2349
+ if self.where_clause
2350
+ else None
2351
+ ),
2352
+ having_clause=(
2353
+ self.having_clause.with_merge(source, target, modifiers)
2354
+ if self.having_clause
2355
+ else None
2356
+ ),
2357
+ )
2358
+ return new
2359
+
2360
+ def with_namespace(self, namespace: str) -> "MultiSelectLineage":
2361
+ return MultiSelectLineage.model_construct(
2362
+ selects=[c.with_namespace(namespace) for c in self.selects],
2363
+ align=self.align.with_namespace(namespace),
2364
+ derive=self.derive.with_namespace(namespace) if self.derive else None,
2365
+ namespace=namespace,
2366
+ hidden_components=self.hidden_components,
2367
+ order_by=self.order_by.with_namespace(namespace) if self.order_by else None,
2368
+ limit=self.limit,
2369
+ where_clause=(
2370
+ self.where_clause.with_namespace(namespace)
2371
+ if self.where_clause
2372
+ else None
2373
+ ),
2374
+ having_clause=(
2375
+ self.having_clause.with_namespace(namespace)
2376
+ if self.having_clause
2377
+ else None
2378
+ ),
2379
+ )
2380
+
2381
+ @property
2382
+ def derived_concepts(self) -> set[str]:
2383
+ output = set()
2384
+ for item in self.align.items:
2385
+ output.add(item.aligned_concept)
2386
+ if self.derive:
2387
+ for ditem in self.derive.items:
2388
+ output.add(ditem.derived_concept)
2389
+ return output
2390
+
2391
+ @property
2392
+ def concept_arguments(self):
2393
+ output = []
2394
+ for select in self.selects:
2395
+ output += select.output_components
2396
+ return unique(output, "address")
2397
+
2398
+
2399
+ class LooseConceptList(BaseModel):
2400
+ concepts: Sequence[Concept | ConceptRef]
2401
+
2402
+ @cached_property
2403
+ def addresses(self) -> set[str]:
2404
+ return {s.address for s in self.concepts}
2405
+
2406
+ @classmethod
2407
+ def validate(cls, v):
2408
+ return cls(v)
2409
+
2410
+ @cached_property
2411
+ def sorted_addresses(self) -> List[str]:
2412
+ return sorted(list(self.addresses))
2413
+
2414
+ def __str__(self) -> str:
2415
+ return f"lcl{str(self.sorted_addresses)}"
2416
+
2417
+ def __iter__(self):
2418
+ return iter(self.concepts)
2419
+
2420
+ def __eq__(self, other):
2421
+ if not isinstance(other, LooseConceptList):
2422
+ return False
2423
+ return self.addresses == other.addresses
2424
+
2425
+ def issubset(self, other):
2426
+ if not isinstance(other, LooseConceptList):
2427
+ return False
2428
+ return self.addresses.issubset(other.addresses)
2429
+
2430
+ def __contains__(self, other):
2431
+ if isinstance(other, str):
2432
+ return other in self.addresses
2433
+ if not isinstance(other, Concept):
2434
+ return False
2435
+ return other.address in self.addresses
2436
+
2437
+ def difference(self, other):
2438
+ if not isinstance(other, LooseConceptList):
2439
+ return False
2440
+ return self.addresses.difference(other.addresses)
2441
+
2442
+ def isdisjoint(self, other):
2443
+ if not isinstance(other, LooseConceptList):
2444
+ return False
2445
+ return self.addresses.isdisjoint(other.addresses)
2446
+
2447
+
2448
+ class AlignItem(Namespaced, BaseModel):
2449
+ alias: str
2450
+ concepts: List[ConceptRef]
2451
+ namespace: str = Field(default=DEFAULT_NAMESPACE, validate_default=True)
2452
+
2453
+ @field_validator("concepts", mode="before")
2454
+ @classmethod
2455
+ def enforce_concept_ref(cls, v):
2456
+ output = []
2457
+ for item in v:
2458
+ if isinstance(item, Concept):
2459
+ output.append(item.reference)
2460
+ else:
2461
+ output.append(item)
2462
+ return output
2463
+
2464
+ @computed_field # type: ignore
2465
+ @cached_property
2466
+ def concepts_lcl(self) -> LooseConceptList:
2467
+ return LooseConceptList(concepts=self.concepts)
2468
+
2469
+ @property
2470
+ def aligned_concept(self) -> str:
2471
+ return f"{self.namespace}.{self.alias}"
2472
+
2473
+ def with_namespace(self, namespace: str) -> "AlignItem":
2474
+ return AlignItem.model_construct(
2475
+ alias=self.alias,
2476
+ concepts=[c.with_namespace(namespace) for c in self.concepts],
2477
+ namespace=namespace,
2478
+ )
2479
+
2480
+
2481
+ class CustomFunctionFactory:
2482
+ def __init__(
2483
+ self,
2484
+ function: Expr,
2485
+ namespace: str,
2486
+ function_arguments: list[ArgBinding],
2487
+ name: str,
2488
+ ):
2489
+ self.namespace = namespace
2490
+ self.function = function
2491
+ self.function_arguments = function_arguments
2492
+ self.name = name
2493
+
2494
+ def with_namespace(self, namespace: str):
2495
+ self.namespace = namespace
2496
+ self.function = (
2497
+ self.function.with_namespace(namespace)
2498
+ if isinstance(self.function, Namespaced)
2499
+ else self.function
2500
+ )
2501
+ self.function_arguments = [
2502
+ x.with_namespace(namespace) for x in self.function_arguments
2503
+ ]
2504
+ return self
2505
+
2506
+ def __call__(self, *creation_args: ArgBinding | Expr):
2507
+ nout = (
2508
+ self.function.model_copy(deep=True)
2509
+ if isinstance(self.function, BaseModel)
2510
+ else self.function
2511
+ )
2512
+ creation_arg_list: list[ArgBinding | Expr] = list(creation_args)
2513
+ if len(creation_args) < len(self.function_arguments):
2514
+ for binding in self.function_arguments[len(creation_arg_list) :]:
2515
+ if binding.default is None:
2516
+ raise ValueError(f"Missing argument {binding.name}")
2517
+
2518
+ creation_arg_list.append(binding.default)
2519
+ for arg_idx, arg in enumerate(self.function_arguments):
2520
+ if not arg.datatype or arg.datatype == DataType.UNKNOWN:
2521
+ continue
2522
+ if arg_idx > len(creation_arg_list):
2523
+ continue
2524
+ comparison = arg_to_datatype(creation_arg_list[arg_idx])
2525
+ if comparison != arg.datatype:
2526
+ raise TypeError(
2527
+ f"Invalid type passed into custom function @{self.name} in position {arg_idx+1} for argument {arg.name}, expected {arg.datatype}, got {comparison}"
2528
+ )
2529
+ if isinstance(arg.datatype, TraitDataType):
2530
+ if not (
2531
+ isinstance(comparison, TraitDataType)
2532
+ and all(x in comparison.traits for x in arg.datatype.traits)
2533
+ ):
2534
+ raise TypeError(
2535
+ 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}"
2536
+ )
2537
+
2538
+ if isinstance(nout, Mergeable):
2539
+ for idx, x in enumerate(creation_arg_list):
2540
+ if self.namespace == DEFAULT_NAMESPACE:
2541
+ target = f"{DEFAULT_NAMESPACE}.{self.function_arguments[idx].name}"
2542
+ else:
2543
+ target = self.function_arguments[idx].name
2544
+ nout = nout.with_reference_replacement(target, x)
2545
+ return nout
2546
+
2547
+
2548
+ class Metadata(BaseModel):
2549
+ """Metadata container object.
2550
+ TODO: support arbitrary tags"""
2551
+
2552
+ description: Optional[str] = None
2553
+ line_number: Optional[int] = None
2554
+ concept_source: ConceptSource = ConceptSource.MANUAL
2555
+
2556
+
2557
+ class Window(BaseModel):
2558
+ count: int
2559
+ window_order: WindowOrder
2560
+
2561
+ def __str__(self):
2562
+ return f"Window<{self.window_order}>"
2563
+
2564
+
2565
+ class WindowItemOver(BaseModel):
2566
+ contents: List[ConceptRef]
2567
+
2568
+
2569
+ class WindowItemOrder(BaseModel):
2570
+ contents: List["OrderItem"]
2571
+
2572
+
2573
+ class Comment(BaseModel):
2574
+ text: str
2575
+
2576
+
2577
+ class ArgBinding(Namespaced, DataTyped, BaseModel):
2578
+ name: str
2579
+ default: Expr | None = None
2580
+ datatype: (
2581
+ DataType | MapType | ArrayType | NumericType | StructType | TraitDataType
2582
+ ) = DataType.UNKNOWN
2583
+
2584
+ def with_namespace(self, namespace):
2585
+ return ArgBinding.model_construct(
2586
+ name=address_with_namespace(self.name, namespace),
2587
+ default=(
2588
+ self.default.with_namespace(namespace)
2589
+ if isinstance(self.default, Namespaced)
2590
+ else self.default
2591
+ ),
2592
+ )
2593
+
2594
+ @property
2595
+ def output_datatype(self):
2596
+ if self.default is not None:
2597
+ return arg_to_datatype(self.default)
2598
+ return self.datatype
2599
+
2600
+
2601
+ class CustomType(BaseModel):
2602
+ name: str
2603
+ type: DataType | list[DataType]
2604
+ drop_on: list[FunctionType] = Field(default_factory=list)
2605
+ add_on: list[FunctionType] = Field(default_factory=list)
2606
+
2607
+ def with_namespace(self, namespace: str) -> "CustomType":
2608
+ return CustomType.model_construct(
2609
+ name=address_with_namespace(self.name, namespace),
2610
+ type=self.type,
2611
+ drop_on=self.drop_on,
2612
+ add_on=self.add_on,
2613
+ )
2614
+
2615
+
2616
+ Expr = (
2617
+ MagicConstants
2618
+ | bool
2619
+ | int
2620
+ | str
2621
+ | float
2622
+ | date
2623
+ | datetime
2624
+ | TupleWrapper
2625
+ | ListWrapper
2626
+ | MapWrapper
2627
+ | WindowItem
2628
+ | FilterItem
2629
+ | ConceptRef
2630
+ | Comparison
2631
+ | Conditional
2632
+ | FunctionCallWrapper
2633
+ | Parenthetical
2634
+ | Function
2635
+ | AggregateWrapper
2636
+ | CaseWhen
2637
+ | CaseElse
2638
+ )
2639
+
2640
+ FuncArgs = (
2641
+ ConceptRef
2642
+ | AggregateWrapper
2643
+ | Function
2644
+ | FunctionCallWrapper
2645
+ | Parenthetical
2646
+ | CaseWhen
2647
+ | CaseElse
2648
+ | WindowItem
2649
+ | FilterItem
2650
+ | bool
2651
+ | int
2652
+ | float
2653
+ | DatePart
2654
+ | str
2655
+ | date
2656
+ | datetime
2657
+ | MapWrapper[Any, Any]
2658
+ | TraitDataType
2659
+ | DataType
2660
+ | ArrayType
2661
+ | MapType
2662
+ | NumericType
2663
+ | ListWrapper[Any]
2664
+ | TupleWrapper[Any]
2665
+ | Comparison
2666
+ | Conditional
2667
+ | MagicConstants
2668
+ | ArgBinding
2669
+ | Ordering
2670
+ )