pytrilogy 0.3.148__cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.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 (206) hide show
  1. LICENSE.md +19 -0
  2. _preql_import_resolver/__init__.py +5 -0
  3. _preql_import_resolver/_preql_import_resolver.cpython-312-aarch64-linux-gnu.so +0 -0
  4. pytrilogy-0.3.148.dist-info/METADATA +555 -0
  5. pytrilogy-0.3.148.dist-info/RECORD +206 -0
  6. pytrilogy-0.3.148.dist-info/WHEEL +5 -0
  7. pytrilogy-0.3.148.dist-info/entry_points.txt +2 -0
  8. pytrilogy-0.3.148.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 +2662 -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 +434 -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 +786 -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 +1431 -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 +376 -0
  117. trilogy/dialect/enums.py +149 -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/file_state_store.py +0 -0
  130. trilogy/execution/state/sqllite_state_store.py +0 -0
  131. trilogy/execution/state/state_store.py +301 -0
  132. trilogy/executor.py +656 -0
  133. trilogy/hooks/__init__.py +4 -0
  134. trilogy/hooks/base_hook.py +40 -0
  135. trilogy/hooks/graph_hook.py +135 -0
  136. trilogy/hooks/query_debugger.py +166 -0
  137. trilogy/metadata/__init__.py +0 -0
  138. trilogy/parser.py +10 -0
  139. trilogy/parsing/README.md +21 -0
  140. trilogy/parsing/__init__.py +0 -0
  141. trilogy/parsing/common.py +1069 -0
  142. trilogy/parsing/config.py +5 -0
  143. trilogy/parsing/exceptions.py +8 -0
  144. trilogy/parsing/helpers.py +1 -0
  145. trilogy/parsing/parse_engine.py +2863 -0
  146. trilogy/parsing/render.py +773 -0
  147. trilogy/parsing/trilogy.lark +544 -0
  148. trilogy/py.typed +0 -0
  149. trilogy/render.py +45 -0
  150. trilogy/scripts/README.md +9 -0
  151. trilogy/scripts/__init__.py +0 -0
  152. trilogy/scripts/agent.py +41 -0
  153. trilogy/scripts/agent_info.py +306 -0
  154. trilogy/scripts/common.py +430 -0
  155. trilogy/scripts/dependency/Cargo.lock +617 -0
  156. trilogy/scripts/dependency/Cargo.toml +39 -0
  157. trilogy/scripts/dependency/README.md +131 -0
  158. trilogy/scripts/dependency/build.sh +25 -0
  159. trilogy/scripts/dependency/src/directory_resolver.rs +387 -0
  160. trilogy/scripts/dependency/src/lib.rs +16 -0
  161. trilogy/scripts/dependency/src/main.rs +770 -0
  162. trilogy/scripts/dependency/src/parser.rs +435 -0
  163. trilogy/scripts/dependency/src/preql.pest +208 -0
  164. trilogy/scripts/dependency/src/python_bindings.rs +311 -0
  165. trilogy/scripts/dependency/src/resolver.rs +716 -0
  166. trilogy/scripts/dependency/tests/base.preql +3 -0
  167. trilogy/scripts/dependency/tests/cli_integration.rs +377 -0
  168. trilogy/scripts/dependency/tests/customer.preql +6 -0
  169. trilogy/scripts/dependency/tests/main.preql +9 -0
  170. trilogy/scripts/dependency/tests/orders.preql +7 -0
  171. trilogy/scripts/dependency/tests/test_data/base.preql +9 -0
  172. trilogy/scripts/dependency/tests/test_data/consumer.preql +1 -0
  173. trilogy/scripts/dependency.py +323 -0
  174. trilogy/scripts/display.py +555 -0
  175. trilogy/scripts/environment.py +59 -0
  176. trilogy/scripts/fmt.py +32 -0
  177. trilogy/scripts/ingest.py +472 -0
  178. trilogy/scripts/ingest_helpers/__init__.py +1 -0
  179. trilogy/scripts/ingest_helpers/foreign_keys.py +123 -0
  180. trilogy/scripts/ingest_helpers/formatting.py +93 -0
  181. trilogy/scripts/ingest_helpers/typing.py +161 -0
  182. trilogy/scripts/init.py +105 -0
  183. trilogy/scripts/parallel_execution.py +748 -0
  184. trilogy/scripts/plan.py +189 -0
  185. trilogy/scripts/refresh.py +106 -0
  186. trilogy/scripts/run.py +79 -0
  187. trilogy/scripts/serve.py +202 -0
  188. trilogy/scripts/serve_helpers/__init__.py +41 -0
  189. trilogy/scripts/serve_helpers/file_discovery.py +142 -0
  190. trilogy/scripts/serve_helpers/index_generation.py +206 -0
  191. trilogy/scripts/serve_helpers/models.py +38 -0
  192. trilogy/scripts/single_execution.py +131 -0
  193. trilogy/scripts/testing.py +129 -0
  194. trilogy/scripts/trilogy.py +75 -0
  195. trilogy/std/__init__.py +0 -0
  196. trilogy/std/color.preql +3 -0
  197. trilogy/std/date.preql +13 -0
  198. trilogy/std/display.preql +18 -0
  199. trilogy/std/geography.preql +22 -0
  200. trilogy/std/metric.preql +15 -0
  201. trilogy/std/money.preql +67 -0
  202. trilogy/std/net.preql +14 -0
  203. trilogy/std/ranking.preql +7 -0
  204. trilogy/std/report.preql +5 -0
  205. trilogy/std/semantic.preql +6 -0
  206. trilogy/utility.py +34 -0
@@ -0,0 +1,434 @@
1
+ from dataclasses import dataclass, field
2
+ from datetime import date, datetime
3
+ from enum import Enum
4
+ from typing import TYPE_CHECKING, ItemsView, List, Optional, Union, ValuesView
5
+
6
+ from pydantic import BaseModel, Field, ValidationInfo, field_validator
7
+
8
+ from trilogy.constants import DEFAULT_NAMESPACE, MagicConstants, logger
9
+ from trilogy.core.enums import (
10
+ AddressType,
11
+ BooleanOperator,
12
+ ComparisonOperator,
13
+ DatasourceState,
14
+ Modifier,
15
+ )
16
+ from trilogy.core.models.author import (
17
+ Comparison,
18
+ Concept,
19
+ ConceptRef,
20
+ Conditional,
21
+ Function,
22
+ Grain,
23
+ HasUUID,
24
+ LooseConceptList,
25
+ Namespaced,
26
+ WhereClause,
27
+ )
28
+
29
+ LOGGER_PREFIX = "[MODELS_DATASOURCE]"
30
+
31
+ if TYPE_CHECKING:
32
+ from trilogy.core.models.environment import Environment
33
+ from trilogy.core.statements.author import SelectStatement
34
+
35
+
36
+ class UpdateKeyType(Enum):
37
+ INCREMENTAL_KEY = "incremental_key"
38
+ UPDATE_TIME = "update_time"
39
+ KEY_HASH = "key_hash"
40
+
41
+
42
+ @dataclass
43
+ class UpdateKey:
44
+ """Represents a key used to track data freshness for incremental updates."""
45
+
46
+ concept_name: str
47
+ type: UpdateKeyType
48
+ value: str | int | float | datetime | date | None
49
+
50
+ def to_comparison(self, environment: "Environment") -> "Comparison":
51
+ """Convert this update key to a Comparison for use in WHERE clauses."""
52
+
53
+ concept = environment.concepts[self.concept_name]
54
+ right_value = self.value if self.value is not None else MagicConstants.NULL
55
+ return Comparison(
56
+ left=concept.reference,
57
+ right=right_value,
58
+ operator=ComparisonOperator.GT,
59
+ )
60
+
61
+
62
+ @dataclass
63
+ class UpdateKeys:
64
+ """Collection of update keys for a datasource."""
65
+
66
+ keys: dict[str, UpdateKey] = field(default_factory=dict)
67
+
68
+ def to_where_clause(self, environment: "Environment") -> WhereClause | None:
69
+ """Convert update keys to a WhereClause for filtering."""
70
+
71
+ comparisons = [
72
+ key.to_comparison(environment)
73
+ for key in self.keys.values()
74
+ if key.value is not None
75
+ ]
76
+ if not comparisons:
77
+ return None
78
+ if len(comparisons) == 1:
79
+ return WhereClause(conditional=comparisons[0])
80
+ conditional = Conditional(
81
+ left=comparisons[0],
82
+ right=comparisons[1],
83
+ operator=BooleanOperator.AND,
84
+ )
85
+ for comp in comparisons[2:]:
86
+ conditional = Conditional(
87
+ left=conditional, right=comp, operator=BooleanOperator.AND
88
+ )
89
+ return WhereClause(conditional=conditional)
90
+
91
+
92
+ class RawColumnExpr(BaseModel):
93
+ text: str
94
+
95
+
96
+ class ColumnAssignment(BaseModel):
97
+ alias: str | RawColumnExpr | Function
98
+ concept: ConceptRef
99
+ modifiers: List[Modifier] = Field(default_factory=list)
100
+
101
+ @field_validator("concept", mode="before")
102
+ def force_reference(cls, v: ConceptRef, info: ValidationInfo):
103
+ if isinstance(v, Concept):
104
+ return v.reference
105
+ return v
106
+
107
+ def __eq__(self, other):
108
+ if not isinstance(other, ColumnAssignment):
109
+ return False
110
+ return (
111
+ self.alias == other.alias
112
+ and self.concept == other.concept
113
+ and self.modifiers == other.modifiers
114
+ )
115
+
116
+ @property
117
+ def is_concrete(self) -> bool:
118
+ return isinstance(self.alias, str)
119
+
120
+ @property
121
+ def is_complete(self) -> bool:
122
+ return Modifier.PARTIAL not in self.modifiers
123
+
124
+ @property
125
+ def is_nullable(self) -> bool:
126
+ return Modifier.NULLABLE in self.modifiers
127
+
128
+ def with_namespace(self, namespace: str) -> "ColumnAssignment":
129
+ return ColumnAssignment.model_construct(
130
+ alias=(
131
+ self.alias.with_namespace(namespace)
132
+ if isinstance(self.alias, Function)
133
+ else self.alias
134
+ ),
135
+ concept=self.concept.with_namespace(namespace),
136
+ modifiers=self.modifiers,
137
+ )
138
+
139
+ def with_merge(
140
+ self, source: Concept, target: Concept, modifiers: List[Modifier]
141
+ ) -> "ColumnAssignment":
142
+ return ColumnAssignment.model_construct(
143
+ alias=self.alias,
144
+ concept=self.concept.with_merge(source, target, modifiers),
145
+ modifiers=(
146
+ modifiers if self.concept.address == source.address else self.modifiers
147
+ ),
148
+ )
149
+
150
+
151
+ class Address(BaseModel):
152
+ location: str
153
+ quoted: bool = False
154
+ type: AddressType = AddressType.TABLE
155
+
156
+ @property
157
+ def is_query(self):
158
+ return self.type == AddressType.QUERY
159
+
160
+ @property
161
+ def is_file(self):
162
+ return self.type in {
163
+ AddressType.PYTHON_SCRIPT,
164
+ AddressType.CSV,
165
+ AddressType.TSV,
166
+ AddressType.PARQUET,
167
+ AddressType.SQL,
168
+ }
169
+
170
+
171
+ @dataclass
172
+ class Query:
173
+ text: str
174
+
175
+
176
+ @dataclass
177
+ class File:
178
+ path: str
179
+ type: AddressType
180
+
181
+
182
+ class DatasourceMetadata(BaseModel):
183
+ freshness_concept: Concept | None
184
+ partition_fields: List[Concept] = Field(default_factory=list)
185
+ line_no: int | None = None
186
+
187
+
188
+ def safe_grain(v) -> Grain:
189
+ if isinstance(v, dict):
190
+ return Grain.model_validate(v)
191
+ elif isinstance(v, Grain):
192
+ return v
193
+ elif not v:
194
+ return Grain(components=set())
195
+ else:
196
+ raise ValueError(f"Invalid input type to safe_grain {type(v)}")
197
+
198
+
199
+ class Datasource(HasUUID, Namespaced, BaseModel):
200
+ name: str
201
+ columns: List[ColumnAssignment]
202
+ address: Union[Address, str]
203
+ grain: Grain = Field(
204
+ default_factory=lambda: Grain(components=set()), validate_default=True
205
+ )
206
+ namespace: Optional[str] = Field(default=DEFAULT_NAMESPACE, validate_default=True)
207
+ metadata: DatasourceMetadata = Field(
208
+ default_factory=lambda: DatasourceMetadata(freshness_concept=None)
209
+ )
210
+ where: Optional[WhereClause] = None
211
+ non_partial_for: Optional[WhereClause] = None
212
+ status: DatasourceState = Field(default=DatasourceState.PUBLISHED)
213
+ incremental_by: List[ConceptRef] = Field(default_factory=list)
214
+ partition_by: List[ConceptRef] = Field(default_factory=list)
215
+ is_root: bool = False
216
+
217
+ @property
218
+ def safe_address(self) -> str:
219
+ if isinstance(self.address, Address):
220
+ return self.address.location
221
+ return self.address
222
+
223
+ def __eq__(self, other):
224
+ if not isinstance(other, Datasource):
225
+ return False
226
+ return (
227
+ self.name == other.name
228
+ and self.namespace == other.namespace
229
+ and self.grain == other.grain
230
+ and self.address == other.address
231
+ and self.where == other.where
232
+ and self.columns == other.columns
233
+ and self.non_partial_for == other.non_partial_for
234
+ )
235
+
236
+ def duplicate(self) -> "Datasource":
237
+ return self.model_copy(deep=True)
238
+
239
+ @property
240
+ def concrete_columns(self) -> dict[str, ColumnAssignment]:
241
+ return {c.alias: c for c in self.columns if c.is_concrete} # type: ignore[misc]
242
+
243
+ @property
244
+ def hidden_concepts(self) -> List[Concept]:
245
+ return []
246
+
247
+ def merge_concept(
248
+ self, source: Concept, target: Concept, modifiers: List[Modifier]
249
+ ):
250
+ original = [c for c in self.columns if c.concept.address == source.address]
251
+ early_exit_check = [
252
+ c for c in self.columns if c.concept.address == target.address
253
+ ]
254
+ if early_exit_check:
255
+ logger.info(
256
+ f"No concept merge needed on merge of {source} to {target}, have {[x.concept.address for x in self.columns]}"
257
+ )
258
+ return None
259
+ if len(original) != 1:
260
+ raise ValueError(
261
+ f"Expected exactly one column to merge, got {len(original)} for {source.address}, {[x.alias for x in original]}"
262
+ )
263
+ # map to the alias with the modifier, and the original
264
+ self.columns = [
265
+ c.with_merge(source, target, modifiers)
266
+ for c in self.columns
267
+ if c.concept.address != source.address
268
+ ] + original
269
+ self.grain = self.grain.with_merge(source, target, modifiers)
270
+ self.where = (
271
+ self.where.with_merge(source, target, modifiers) if self.where else None
272
+ )
273
+
274
+ self.add_column(target, original[0].alias, modifiers)
275
+
276
+ @property
277
+ def identifier(self) -> str:
278
+ if not self.namespace or self.namespace == DEFAULT_NAMESPACE:
279
+ return self.name
280
+ return f"{self.namespace}.{self.name}"
281
+
282
+ @property
283
+ def safe_identifier(self) -> str:
284
+ return self.identifier.replace(".", "_")
285
+
286
+ @property
287
+ def output_lcl(self) -> LooseConceptList:
288
+ return LooseConceptList(concepts=self.output_concepts)
289
+
290
+ @property
291
+ def non_partial_concept_addresses(self) -> set[str]:
292
+ return set([c.address for c in self.full_concepts])
293
+
294
+ @field_validator("namespace", mode="plain")
295
+ @classmethod
296
+ def namespace_validation(cls, v):
297
+ return v or DEFAULT_NAMESPACE
298
+
299
+ @field_validator("address")
300
+ @classmethod
301
+ def address_enforcement(cls, v):
302
+ if isinstance(v, str):
303
+ v = Address(location=v)
304
+ return v
305
+
306
+ @field_validator("grain", mode="before")
307
+ @classmethod
308
+ def grain_enforcement(cls, v: Grain, info: ValidationInfo):
309
+ grain: Grain = safe_grain(v)
310
+ return grain
311
+
312
+ def add_column(
313
+ self,
314
+ concept: Concept,
315
+ alias: str | RawColumnExpr | Function,
316
+ modifiers: List[Modifier] | None = None,
317
+ ):
318
+ self.columns.append(
319
+ ColumnAssignment(
320
+ alias=alias, concept=concept.reference, modifiers=modifiers or []
321
+ )
322
+ )
323
+
324
+ def __add__(self, other):
325
+ if not other == self:
326
+ raise ValueError(
327
+ "Attempted to add two datasources that are not identical, this is not a valid operation"
328
+ )
329
+ return self
330
+
331
+ def __repr__(self):
332
+ return f"Datasource<{self.identifier}@<{self.grain}>"
333
+
334
+ def __str__(self):
335
+ return self.__repr__()
336
+
337
+ def __hash__(self):
338
+ return self.identifier.__hash__()
339
+
340
+ def with_namespace(self, namespace: str):
341
+ new_namespace = (
342
+ namespace + "." + self.namespace
343
+ if self.namespace and self.namespace != DEFAULT_NAMESPACE
344
+ else namespace
345
+ )
346
+ new = Datasource.model_construct(
347
+ name=self.name,
348
+ namespace=new_namespace,
349
+ grain=self.grain.with_namespace(namespace),
350
+ address=self.address,
351
+ columns=[c.with_namespace(namespace) for c in self.columns],
352
+ where=self.where.with_namespace(namespace) if self.where else None,
353
+ non_partial_for=(
354
+ self.non_partial_for.with_namespace(namespace)
355
+ if self.non_partial_for
356
+ else None
357
+ ),
358
+ status=self.status,
359
+ incremental_by=[c.with_namespace(namespace) for c in self.incremental_by],
360
+ partition_by=[c.with_namespace(namespace) for c in self.partition_by],
361
+ is_root=self.is_root,
362
+ )
363
+ return new
364
+
365
+ def create_update_statement(
366
+ self,
367
+ environment: "Environment",
368
+ where: Optional[WhereClause] = None,
369
+ line_no: int | None = None,
370
+ ) -> "SelectStatement":
371
+ from trilogy.core.statements.author import Metadata, SelectItem, SelectStatement
372
+
373
+ return SelectStatement.from_inputs(
374
+ environment=environment,
375
+ selection=[
376
+ SelectItem(
377
+ content=ConceptRef(address=col.concept.address),
378
+ modifiers=[],
379
+ )
380
+ for col in self.columns
381
+ ],
382
+ where_clause=where,
383
+ meta=Metadata(line_number=line_no) if line_no else None,
384
+ )
385
+
386
+ @property
387
+ def concepts(self) -> List[ConceptRef]:
388
+ return [c.concept for c in self.columns]
389
+
390
+ @property
391
+ def group_required(self):
392
+ return False
393
+
394
+ @property
395
+ def full_concepts(self) -> List[ConceptRef]:
396
+ return [c.concept for c in self.columns if Modifier.PARTIAL not in c.modifiers]
397
+
398
+ @property
399
+ def nullable_concepts(self) -> List[ConceptRef]:
400
+ return [c.concept for c in self.columns if Modifier.NULLABLE in c.modifiers]
401
+
402
+ @property
403
+ def output_concepts(self) -> List[ConceptRef]:
404
+ return self.concepts
405
+
406
+ @property
407
+ def partial_concepts(self) -> List[ConceptRef]:
408
+ return [c.concept for c in self.columns if Modifier.PARTIAL in c.modifiers]
409
+
410
+
411
+ class EnvironmentDatasourceDict(dict):
412
+ def __init__(self, *args, **kwargs) -> None:
413
+ super().__init__(self, *args, **kwargs)
414
+
415
+ def __getitem__(self, key: str) -> Datasource:
416
+ try:
417
+ return super(EnvironmentDatasourceDict, self).__getitem__(key)
418
+ except KeyError:
419
+ if DEFAULT_NAMESPACE + "." + key in self:
420
+ return self.__getitem__(DEFAULT_NAMESPACE + "." + key)
421
+ if "." in key and key.split(".", 1)[0] == DEFAULT_NAMESPACE:
422
+ return self.__getitem__(key.split(".", 1)[1])
423
+ raise
424
+
425
+ def values(self) -> ValuesView[Datasource]: # type: ignore
426
+ return super().values()
427
+
428
+ def items(self) -> ItemsView[str, Datasource]: # type: ignore
429
+ return super().items()
430
+
431
+ def duplicate(self) -> "EnvironmentDatasourceDict":
432
+ new = EnvironmentDatasourceDict()
433
+ new.update({k: v.duplicate() for k, v in self.items()})
434
+ return new