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.
- LICENSE.md +19 -0
- _preql_import_resolver/__init__.py +5 -0
- _preql_import_resolver/_preql_import_resolver.cpython-312-aarch64-linux-gnu.so +0 -0
- pytrilogy-0.3.148.dist-info/METADATA +555 -0
- pytrilogy-0.3.148.dist-info/RECORD +206 -0
- pytrilogy-0.3.148.dist-info/WHEEL +5 -0
- pytrilogy-0.3.148.dist-info/entry_points.txt +2 -0
- pytrilogy-0.3.148.dist-info/licenses/LICENSE.md +19 -0
- trilogy/__init__.py +27 -0
- trilogy/ai/README.md +10 -0
- trilogy/ai/__init__.py +19 -0
- trilogy/ai/constants.py +92 -0
- trilogy/ai/conversation.py +107 -0
- trilogy/ai/enums.py +7 -0
- trilogy/ai/execute.py +50 -0
- trilogy/ai/models.py +34 -0
- trilogy/ai/prompts.py +100 -0
- trilogy/ai/providers/__init__.py +0 -0
- trilogy/ai/providers/anthropic.py +106 -0
- trilogy/ai/providers/base.py +24 -0
- trilogy/ai/providers/google.py +146 -0
- trilogy/ai/providers/openai.py +89 -0
- trilogy/ai/providers/utils.py +68 -0
- trilogy/authoring/README.md +3 -0
- trilogy/authoring/__init__.py +148 -0
- trilogy/constants.py +119 -0
- trilogy/core/README.md +52 -0
- trilogy/core/__init__.py +0 -0
- trilogy/core/constants.py +6 -0
- trilogy/core/enums.py +454 -0
- trilogy/core/env_processor.py +239 -0
- trilogy/core/environment_helpers.py +320 -0
- trilogy/core/ergonomics.py +193 -0
- trilogy/core/exceptions.py +123 -0
- trilogy/core/functions.py +1240 -0
- trilogy/core/graph_models.py +142 -0
- trilogy/core/internal.py +85 -0
- trilogy/core/models/__init__.py +0 -0
- trilogy/core/models/author.py +2662 -0
- trilogy/core/models/build.py +2603 -0
- trilogy/core/models/build_environment.py +165 -0
- trilogy/core/models/core.py +506 -0
- trilogy/core/models/datasource.py +434 -0
- trilogy/core/models/environment.py +756 -0
- trilogy/core/models/execute.py +1213 -0
- trilogy/core/optimization.py +251 -0
- trilogy/core/optimizations/__init__.py +12 -0
- trilogy/core/optimizations/base_optimization.py +17 -0
- trilogy/core/optimizations/hide_unused_concept.py +47 -0
- trilogy/core/optimizations/inline_datasource.py +102 -0
- trilogy/core/optimizations/predicate_pushdown.py +245 -0
- trilogy/core/processing/README.md +94 -0
- trilogy/core/processing/READMEv2.md +121 -0
- trilogy/core/processing/VIRTUAL_UNNEST.md +30 -0
- trilogy/core/processing/__init__.py +0 -0
- trilogy/core/processing/concept_strategies_v3.py +508 -0
- trilogy/core/processing/constants.py +15 -0
- trilogy/core/processing/discovery_node_factory.py +451 -0
- trilogy/core/processing/discovery_utility.py +548 -0
- trilogy/core/processing/discovery_validation.py +167 -0
- trilogy/core/processing/graph_utils.py +43 -0
- trilogy/core/processing/node_generators/README.md +9 -0
- trilogy/core/processing/node_generators/__init__.py +31 -0
- trilogy/core/processing/node_generators/basic_node.py +160 -0
- trilogy/core/processing/node_generators/common.py +270 -0
- trilogy/core/processing/node_generators/constant_node.py +38 -0
- trilogy/core/processing/node_generators/filter_node.py +315 -0
- trilogy/core/processing/node_generators/group_node.py +213 -0
- trilogy/core/processing/node_generators/group_to_node.py +117 -0
- trilogy/core/processing/node_generators/multiselect_node.py +207 -0
- trilogy/core/processing/node_generators/node_merge_node.py +695 -0
- trilogy/core/processing/node_generators/recursive_node.py +88 -0
- trilogy/core/processing/node_generators/rowset_node.py +165 -0
- trilogy/core/processing/node_generators/select_helpers/__init__.py +0 -0
- trilogy/core/processing/node_generators/select_helpers/datasource_injection.py +261 -0
- trilogy/core/processing/node_generators/select_merge_node.py +786 -0
- trilogy/core/processing/node_generators/select_node.py +95 -0
- trilogy/core/processing/node_generators/synonym_node.py +98 -0
- trilogy/core/processing/node_generators/union_node.py +91 -0
- trilogy/core/processing/node_generators/unnest_node.py +182 -0
- trilogy/core/processing/node_generators/window_node.py +201 -0
- trilogy/core/processing/nodes/README.md +28 -0
- trilogy/core/processing/nodes/__init__.py +179 -0
- trilogy/core/processing/nodes/base_node.py +522 -0
- trilogy/core/processing/nodes/filter_node.py +75 -0
- trilogy/core/processing/nodes/group_node.py +194 -0
- trilogy/core/processing/nodes/merge_node.py +420 -0
- trilogy/core/processing/nodes/recursive_node.py +46 -0
- trilogy/core/processing/nodes/select_node_v2.py +242 -0
- trilogy/core/processing/nodes/union_node.py +53 -0
- trilogy/core/processing/nodes/unnest_node.py +62 -0
- trilogy/core/processing/nodes/window_node.py +56 -0
- trilogy/core/processing/utility.py +823 -0
- trilogy/core/query_processor.py +604 -0
- trilogy/core/statements/README.md +35 -0
- trilogy/core/statements/__init__.py +0 -0
- trilogy/core/statements/author.py +536 -0
- trilogy/core/statements/build.py +0 -0
- trilogy/core/statements/common.py +20 -0
- trilogy/core/statements/execute.py +155 -0
- trilogy/core/table_processor.py +66 -0
- trilogy/core/utility.py +8 -0
- trilogy/core/validation/README.md +46 -0
- trilogy/core/validation/__init__.py +0 -0
- trilogy/core/validation/common.py +161 -0
- trilogy/core/validation/concept.py +146 -0
- trilogy/core/validation/datasource.py +227 -0
- trilogy/core/validation/environment.py +73 -0
- trilogy/core/validation/fix.py +256 -0
- trilogy/dialect/__init__.py +32 -0
- trilogy/dialect/base.py +1431 -0
- trilogy/dialect/bigquery.py +314 -0
- trilogy/dialect/common.py +147 -0
- trilogy/dialect/config.py +159 -0
- trilogy/dialect/dataframe.py +50 -0
- trilogy/dialect/duckdb.py +376 -0
- trilogy/dialect/enums.py +149 -0
- trilogy/dialect/metadata.py +173 -0
- trilogy/dialect/mock.py +190 -0
- trilogy/dialect/postgres.py +117 -0
- trilogy/dialect/presto.py +110 -0
- trilogy/dialect/results.py +89 -0
- trilogy/dialect/snowflake.py +129 -0
- trilogy/dialect/sql_server.py +137 -0
- trilogy/engine.py +48 -0
- trilogy/execution/__init__.py +17 -0
- trilogy/execution/config.py +119 -0
- trilogy/execution/state/__init__.py +0 -0
- trilogy/execution/state/file_state_store.py +0 -0
- trilogy/execution/state/sqllite_state_store.py +0 -0
- trilogy/execution/state/state_store.py +301 -0
- trilogy/executor.py +656 -0
- trilogy/hooks/__init__.py +4 -0
- trilogy/hooks/base_hook.py +40 -0
- trilogy/hooks/graph_hook.py +135 -0
- trilogy/hooks/query_debugger.py +166 -0
- trilogy/metadata/__init__.py +0 -0
- trilogy/parser.py +10 -0
- trilogy/parsing/README.md +21 -0
- trilogy/parsing/__init__.py +0 -0
- trilogy/parsing/common.py +1069 -0
- trilogy/parsing/config.py +5 -0
- trilogy/parsing/exceptions.py +8 -0
- trilogy/parsing/helpers.py +1 -0
- trilogy/parsing/parse_engine.py +2863 -0
- trilogy/parsing/render.py +773 -0
- trilogy/parsing/trilogy.lark +544 -0
- trilogy/py.typed +0 -0
- trilogy/render.py +45 -0
- trilogy/scripts/README.md +9 -0
- trilogy/scripts/__init__.py +0 -0
- trilogy/scripts/agent.py +41 -0
- trilogy/scripts/agent_info.py +306 -0
- trilogy/scripts/common.py +430 -0
- trilogy/scripts/dependency/Cargo.lock +617 -0
- trilogy/scripts/dependency/Cargo.toml +39 -0
- trilogy/scripts/dependency/README.md +131 -0
- trilogy/scripts/dependency/build.sh +25 -0
- trilogy/scripts/dependency/src/directory_resolver.rs +387 -0
- trilogy/scripts/dependency/src/lib.rs +16 -0
- trilogy/scripts/dependency/src/main.rs +770 -0
- trilogy/scripts/dependency/src/parser.rs +435 -0
- trilogy/scripts/dependency/src/preql.pest +208 -0
- trilogy/scripts/dependency/src/python_bindings.rs +311 -0
- trilogy/scripts/dependency/src/resolver.rs +716 -0
- trilogy/scripts/dependency/tests/base.preql +3 -0
- trilogy/scripts/dependency/tests/cli_integration.rs +377 -0
- trilogy/scripts/dependency/tests/customer.preql +6 -0
- trilogy/scripts/dependency/tests/main.preql +9 -0
- trilogy/scripts/dependency/tests/orders.preql +7 -0
- trilogy/scripts/dependency/tests/test_data/base.preql +9 -0
- trilogy/scripts/dependency/tests/test_data/consumer.preql +1 -0
- trilogy/scripts/dependency.py +323 -0
- trilogy/scripts/display.py +555 -0
- trilogy/scripts/environment.py +59 -0
- trilogy/scripts/fmt.py +32 -0
- trilogy/scripts/ingest.py +472 -0
- trilogy/scripts/ingest_helpers/__init__.py +1 -0
- trilogy/scripts/ingest_helpers/foreign_keys.py +123 -0
- trilogy/scripts/ingest_helpers/formatting.py +93 -0
- trilogy/scripts/ingest_helpers/typing.py +161 -0
- trilogy/scripts/init.py +105 -0
- trilogy/scripts/parallel_execution.py +748 -0
- trilogy/scripts/plan.py +189 -0
- trilogy/scripts/refresh.py +106 -0
- trilogy/scripts/run.py +79 -0
- trilogy/scripts/serve.py +202 -0
- trilogy/scripts/serve_helpers/__init__.py +41 -0
- trilogy/scripts/serve_helpers/file_discovery.py +142 -0
- trilogy/scripts/serve_helpers/index_generation.py +206 -0
- trilogy/scripts/serve_helpers/models.py +38 -0
- trilogy/scripts/single_execution.py +131 -0
- trilogy/scripts/testing.py +129 -0
- trilogy/scripts/trilogy.py +75 -0
- trilogy/std/__init__.py +0 -0
- trilogy/std/color.preql +3 -0
- trilogy/std/date.preql +13 -0
- trilogy/std/display.preql +18 -0
- trilogy/std/geography.preql +22 -0
- trilogy/std/metric.preql +15 -0
- trilogy/std/money.preql +67 -0
- trilogy/std/net.preql +14 -0
- trilogy/std/ranking.preql +7 -0
- trilogy/std/report.preql +5 -0
- trilogy/std/semantic.preql +6 -0
- 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
|