metaxy 0.0.1.dev3__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- metaxy/__init__.py +170 -0
- metaxy/_packaging.py +96 -0
- metaxy/_testing/__init__.py +55 -0
- metaxy/_testing/config.py +43 -0
- metaxy/_testing/metaxy_project.py +780 -0
- metaxy/_testing/models.py +111 -0
- metaxy/_testing/parametric/__init__.py +13 -0
- metaxy/_testing/parametric/metadata.py +664 -0
- metaxy/_testing/pytest_helpers.py +74 -0
- metaxy/_testing/runbook.py +533 -0
- metaxy/_utils.py +35 -0
- metaxy/_version.py +1 -0
- metaxy/cli/app.py +97 -0
- metaxy/cli/console.py +13 -0
- metaxy/cli/context.py +167 -0
- metaxy/cli/graph.py +610 -0
- metaxy/cli/graph_diff.py +290 -0
- metaxy/cli/list.py +46 -0
- metaxy/cli/metadata.py +317 -0
- metaxy/cli/migrations.py +999 -0
- metaxy/cli/utils.py +268 -0
- metaxy/config.py +680 -0
- metaxy/entrypoints.py +296 -0
- metaxy/ext/__init__.py +1 -0
- metaxy/ext/dagster/__init__.py +54 -0
- metaxy/ext/dagster/constants.py +10 -0
- metaxy/ext/dagster/dagster_type.py +156 -0
- metaxy/ext/dagster/io_manager.py +200 -0
- metaxy/ext/dagster/metaxify.py +512 -0
- metaxy/ext/dagster/observable.py +115 -0
- metaxy/ext/dagster/resources.py +27 -0
- metaxy/ext/dagster/selection.py +73 -0
- metaxy/ext/dagster/table_metadata.py +417 -0
- metaxy/ext/dagster/utils.py +462 -0
- metaxy/ext/sqlalchemy/__init__.py +23 -0
- metaxy/ext/sqlalchemy/config.py +29 -0
- metaxy/ext/sqlalchemy/plugin.py +353 -0
- metaxy/ext/sqlmodel/__init__.py +13 -0
- metaxy/ext/sqlmodel/config.py +29 -0
- metaxy/ext/sqlmodel/plugin.py +499 -0
- metaxy/graph/__init__.py +29 -0
- metaxy/graph/describe.py +325 -0
- metaxy/graph/diff/__init__.py +21 -0
- metaxy/graph/diff/diff_models.py +446 -0
- metaxy/graph/diff/differ.py +769 -0
- metaxy/graph/diff/models.py +443 -0
- metaxy/graph/diff/rendering/__init__.py +18 -0
- metaxy/graph/diff/rendering/base.py +323 -0
- metaxy/graph/diff/rendering/cards.py +188 -0
- metaxy/graph/diff/rendering/formatter.py +805 -0
- metaxy/graph/diff/rendering/graphviz.py +246 -0
- metaxy/graph/diff/rendering/mermaid.py +326 -0
- metaxy/graph/diff/rendering/rich.py +169 -0
- metaxy/graph/diff/rendering/theme.py +48 -0
- metaxy/graph/diff/traversal.py +247 -0
- metaxy/graph/status.py +329 -0
- metaxy/graph/utils.py +58 -0
- metaxy/metadata_store/__init__.py +32 -0
- metaxy/metadata_store/_ducklake_support.py +419 -0
- metaxy/metadata_store/base.py +1792 -0
- metaxy/metadata_store/bigquery.py +354 -0
- metaxy/metadata_store/clickhouse.py +184 -0
- metaxy/metadata_store/delta.py +371 -0
- metaxy/metadata_store/duckdb.py +446 -0
- metaxy/metadata_store/exceptions.py +61 -0
- metaxy/metadata_store/ibis.py +542 -0
- metaxy/metadata_store/lancedb.py +391 -0
- metaxy/metadata_store/memory.py +292 -0
- metaxy/metadata_store/system/__init__.py +57 -0
- metaxy/metadata_store/system/events.py +264 -0
- metaxy/metadata_store/system/keys.py +9 -0
- metaxy/metadata_store/system/models.py +129 -0
- metaxy/metadata_store/system/storage.py +957 -0
- metaxy/metadata_store/types.py +10 -0
- metaxy/metadata_store/utils.py +104 -0
- metaxy/metadata_store/warnings.py +36 -0
- metaxy/migrations/__init__.py +32 -0
- metaxy/migrations/detector.py +291 -0
- metaxy/migrations/executor.py +516 -0
- metaxy/migrations/generator.py +319 -0
- metaxy/migrations/loader.py +231 -0
- metaxy/migrations/models.py +528 -0
- metaxy/migrations/ops.py +447 -0
- metaxy/models/__init__.py +0 -0
- metaxy/models/bases.py +12 -0
- metaxy/models/constants.py +139 -0
- metaxy/models/feature.py +1335 -0
- metaxy/models/feature_spec.py +338 -0
- metaxy/models/field.py +263 -0
- metaxy/models/fields_mapping.py +307 -0
- metaxy/models/filter_expression.py +297 -0
- metaxy/models/lineage.py +285 -0
- metaxy/models/plan.py +232 -0
- metaxy/models/types.py +475 -0
- metaxy/py.typed +0 -0
- metaxy/utils/__init__.py +1 -0
- metaxy/utils/constants.py +2 -0
- metaxy/utils/exceptions.py +23 -0
- metaxy/utils/hashing.py +230 -0
- metaxy/versioning/__init__.py +31 -0
- metaxy/versioning/engine.py +656 -0
- metaxy/versioning/feature_dep_transformer.py +151 -0
- metaxy/versioning/ibis.py +249 -0
- metaxy/versioning/lineage_handler.py +205 -0
- metaxy/versioning/polars.py +189 -0
- metaxy/versioning/renamed_df.py +35 -0
- metaxy/versioning/types.py +63 -0
- metaxy-0.0.1.dev3.dist-info/METADATA +96 -0
- metaxy-0.0.1.dev3.dist-info/RECORD +111 -0
- metaxy-0.0.1.dev3.dist-info/WHEEL +4 -0
- metaxy-0.0.1.dev3.dist-info/entry_points.txt +4 -0
metaxy/models/types.py
ADDED
|
@@ -0,0 +1,475 @@
|
|
|
1
|
+
"""Type definitions for metaxy models."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Iterator, Sequence
|
|
6
|
+
from typing import TYPE_CHECKING, Annotated, Any, NamedTuple, TypeAlias, overload
|
|
7
|
+
|
|
8
|
+
from pydantic import (
|
|
9
|
+
BeforeValidator,
|
|
10
|
+
ConfigDict,
|
|
11
|
+
Field,
|
|
12
|
+
RootModel,
|
|
13
|
+
TypeAdapter,
|
|
14
|
+
field_validator,
|
|
15
|
+
model_serializer,
|
|
16
|
+
model_validator,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
if TYPE_CHECKING:
|
|
20
|
+
from typing_extensions import Self
|
|
21
|
+
|
|
22
|
+
KEY_SEPARATOR = "/"
|
|
23
|
+
|
|
24
|
+
# backcompat
|
|
25
|
+
FEATURE_KEY_SEPARATOR = KEY_SEPARATOR
|
|
26
|
+
FIELD_KEY_SEPARATOR = KEY_SEPARATOR
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class SnapshotPushResult(NamedTuple):
|
|
30
|
+
"""Result of recording a feature graph snapshot.
|
|
31
|
+
|
|
32
|
+
Attributes:
|
|
33
|
+
snapshot_version: The deterministic hash of the graph snapshot
|
|
34
|
+
already_pushed: True if this snapshot_version was already pushed previously
|
|
35
|
+
updated_features: List of feature keys with updated information (changed full_definition_version)
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
snapshot_version: str
|
|
39
|
+
already_pushed: bool
|
|
40
|
+
updated_features: list[str]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
_CoercibleToKey: TypeAlias = Sequence[str] | str
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class _Key(RootModel[tuple[str, ...]]):
|
|
47
|
+
"""
|
|
48
|
+
A common class for key-like objects that contain a sequence of string parts.
|
|
49
|
+
|
|
50
|
+
Parts cannot contain forward slashes (/) or double underscores (__).
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
key: Feature key as string ("a/b/c"), sequence (["a", "b", "c"]), or FeatureKey instance.
|
|
54
|
+
String format is split on "/" separator.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
model_config = ConfigDict(frozen=True, repr=False) # pyright: ignore[reportCallIssue] # Make immutable for hashability, use custom __repr__
|
|
58
|
+
|
|
59
|
+
root: tuple[str, ...]
|
|
60
|
+
|
|
61
|
+
if TYPE_CHECKING:
|
|
62
|
+
|
|
63
|
+
@overload
|
|
64
|
+
def __init__(self, parts: str) -> None: ...
|
|
65
|
+
|
|
66
|
+
@overload
|
|
67
|
+
def __init__(self, parts: Sequence[str]) -> None: ...
|
|
68
|
+
|
|
69
|
+
@overload
|
|
70
|
+
def __init__(self, parts: Self) -> None: ...
|
|
71
|
+
|
|
72
|
+
def __init__( # pyright: ignore[reportMissingSuperCall]
|
|
73
|
+
self,
|
|
74
|
+
parts: str | Sequence[str] | Self,
|
|
75
|
+
) -> None: ...
|
|
76
|
+
|
|
77
|
+
@model_validator(mode="before")
|
|
78
|
+
@classmethod
|
|
79
|
+
def _validate_input(cls, data: Any) -> Any:
|
|
80
|
+
"""Convert various input types to tuple of strings."""
|
|
81
|
+
# If it's already a tuple, validate and return it
|
|
82
|
+
if isinstance(data, tuple):
|
|
83
|
+
return data
|
|
84
|
+
|
|
85
|
+
# Handle dict input (from Pydantic deserialization)
|
|
86
|
+
if isinstance(data, dict):
|
|
87
|
+
# RootModel deserialization passes dict with "root" key
|
|
88
|
+
if "root" in data:
|
|
89
|
+
root_value = data["root"]
|
|
90
|
+
if isinstance(root_value, tuple):
|
|
91
|
+
return root_value
|
|
92
|
+
elif isinstance(root_value, (list, Sequence)):
|
|
93
|
+
return tuple(root_value)
|
|
94
|
+
# Legacy "parts" key for backward compatibility
|
|
95
|
+
elif "parts" in data:
|
|
96
|
+
parts = data["parts"]
|
|
97
|
+
if (
|
|
98
|
+
isinstance(parts, (list, tuple))
|
|
99
|
+
and parts
|
|
100
|
+
and isinstance(parts[0], dict)
|
|
101
|
+
):
|
|
102
|
+
# Handle incorrectly nested structure like {'parts': [{'parts': [...]}]}
|
|
103
|
+
if "parts" in parts[0]:
|
|
104
|
+
return tuple(parts[0]["parts"])
|
|
105
|
+
else:
|
|
106
|
+
raise ValueError(f"Invalid nested structure in parts: {parts}")
|
|
107
|
+
return tuple(parts) if not isinstance(parts, tuple) else parts
|
|
108
|
+
# Empty dict
|
|
109
|
+
raise ValueError("Dict must contain 'root' or 'parts' key")
|
|
110
|
+
|
|
111
|
+
# Handle string input - split on separator
|
|
112
|
+
if isinstance(data, str):
|
|
113
|
+
return tuple(data.split(KEY_SEPARATOR))
|
|
114
|
+
|
|
115
|
+
# Handle instance of same class - extract root
|
|
116
|
+
if isinstance(data, cls):
|
|
117
|
+
return data.root
|
|
118
|
+
|
|
119
|
+
# Handle sequence (list, etc.)
|
|
120
|
+
if isinstance(data, Sequence):
|
|
121
|
+
return tuple(data)
|
|
122
|
+
|
|
123
|
+
raise ValueError(f"Cannot create {cls.__name__} from {type(data).__name__}")
|
|
124
|
+
|
|
125
|
+
@field_validator("root", mode="after")
|
|
126
|
+
@classmethod
|
|
127
|
+
def _validate_root_content(cls, value: tuple[str, ...]) -> tuple[str, ...]:
|
|
128
|
+
"""Validate that parts don't contain forbidden characters."""
|
|
129
|
+
for part in value:
|
|
130
|
+
if not isinstance(part, str):
|
|
131
|
+
raise ValueError(
|
|
132
|
+
f"{cls.__name__} parts must be strings, got {type(part).__name__}"
|
|
133
|
+
)
|
|
134
|
+
if "/" in part:
|
|
135
|
+
raise ValueError(
|
|
136
|
+
f"{cls.__name__} part '{part}' cannot contain forward slashes (/). "
|
|
137
|
+
f"Forward slashes are reserved as the separator in to_string(). "
|
|
138
|
+
f"Use underscores or hyphens instead."
|
|
139
|
+
)
|
|
140
|
+
if "__" in part:
|
|
141
|
+
raise ValueError(
|
|
142
|
+
f"{cls.__name__} part '{part}' cannot contain double underscores (__). "
|
|
143
|
+
f"Use single underscores or hyphens instead."
|
|
144
|
+
)
|
|
145
|
+
return value
|
|
146
|
+
|
|
147
|
+
@model_serializer
|
|
148
|
+
def _serialize_model(self) -> list[str]:
|
|
149
|
+
"""Serialize to list format for backward compatibility."""
|
|
150
|
+
return list(self.parts)
|
|
151
|
+
|
|
152
|
+
@property
|
|
153
|
+
def parts(self) -> tuple[str, ...]:
|
|
154
|
+
"""Backward compatibility property for accessing root as parts."""
|
|
155
|
+
return self.root
|
|
156
|
+
|
|
157
|
+
def to_string(self) -> str:
|
|
158
|
+
"""Convert to string representation with "/" separator."""
|
|
159
|
+
return KEY_SEPARATOR.join(self.parts)
|
|
160
|
+
|
|
161
|
+
def to_struct_key(self) -> str:
|
|
162
|
+
"""Convert to a name that can be used as struct key in databases"""
|
|
163
|
+
return "_".join(self.parts)
|
|
164
|
+
|
|
165
|
+
def to_column_suffix(self) -> str:
|
|
166
|
+
"""Convert to a suffix usable for database column names (typically temporary)."""
|
|
167
|
+
return "__" + "_".join(self.parts)
|
|
168
|
+
|
|
169
|
+
def __repr__(self) -> str:
|
|
170
|
+
"""Return string representation."""
|
|
171
|
+
return self.to_string()
|
|
172
|
+
|
|
173
|
+
def __str__(self) -> str:
|
|
174
|
+
"""Return string representation."""
|
|
175
|
+
return self.to_string()
|
|
176
|
+
|
|
177
|
+
def __lt__(self, other: Any) -> bool:
|
|
178
|
+
"""Less than comparison for sorting."""
|
|
179
|
+
if isinstance(other, self.__class__):
|
|
180
|
+
return self.parts < other.parts
|
|
181
|
+
return NotImplemented
|
|
182
|
+
|
|
183
|
+
def __le__(self, other: Any) -> bool:
|
|
184
|
+
"""Less than or equal comparison for sorting."""
|
|
185
|
+
if isinstance(other, self.__class__):
|
|
186
|
+
return self.parts <= other.parts
|
|
187
|
+
return NotImplemented
|
|
188
|
+
|
|
189
|
+
def __gt__(self, other: Any) -> bool:
|
|
190
|
+
"""Greater than comparison for sorting."""
|
|
191
|
+
if isinstance(other, self.__class__):
|
|
192
|
+
return self.parts > other.parts
|
|
193
|
+
return NotImplemented
|
|
194
|
+
|
|
195
|
+
def __ge__(self, other: Any) -> bool:
|
|
196
|
+
"""Greater than or equal comparison for sorting."""
|
|
197
|
+
if isinstance(other, self.__class__):
|
|
198
|
+
return self.parts >= other.parts
|
|
199
|
+
return NotImplemented
|
|
200
|
+
|
|
201
|
+
def __iter__(self) -> Iterator[str]: # pyright: ignore[reportIncompatibleMethodOverride]
|
|
202
|
+
"""Return iterator over parts."""
|
|
203
|
+
return iter(self.parts)
|
|
204
|
+
|
|
205
|
+
@property
|
|
206
|
+
def table_name(self) -> str:
|
|
207
|
+
"""Get SQL-like table name for this feature key.
|
|
208
|
+
|
|
209
|
+
Replaces hyphens with underscores for SQL compatibility.
|
|
210
|
+
"""
|
|
211
|
+
return "__".join(part.replace("-", "_") for part in self.parts)
|
|
212
|
+
|
|
213
|
+
# List-like interface for backward compatibility
|
|
214
|
+
def __getitem__(self, index: int) -> str:
|
|
215
|
+
"""Get part by index."""
|
|
216
|
+
return self.parts[index]
|
|
217
|
+
|
|
218
|
+
def __len__(self) -> int:
|
|
219
|
+
"""Get number of parts."""
|
|
220
|
+
return len(self.parts)
|
|
221
|
+
|
|
222
|
+
def __contains__(self, item: str) -> bool:
|
|
223
|
+
"""Check if part is in key."""
|
|
224
|
+
return item in self.parts
|
|
225
|
+
|
|
226
|
+
def __reversed__(self):
|
|
227
|
+
"""Return reversed iterator over parts."""
|
|
228
|
+
return reversed(self.parts)
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
# CoercibleToKey: TypeAlias = _CoercibleToKey | _Key
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
class FeatureKey(_Key):
|
|
235
|
+
"""
|
|
236
|
+
Feature key as a sequence of string parts.
|
|
237
|
+
|
|
238
|
+
Hashable for use as dict keys in registries.
|
|
239
|
+
Parts cannot contain forward slashes (/) or double underscores (__).
|
|
240
|
+
|
|
241
|
+
Example:
|
|
242
|
+
|
|
243
|
+
```py
|
|
244
|
+
FeatureKey("a/b/c") # String format
|
|
245
|
+
# FeatureKey(parts=['a', 'b', 'c'])
|
|
246
|
+
|
|
247
|
+
FeatureKey(["a", "b", "c"]) # List format
|
|
248
|
+
# FeatureKey(parts=['a', 'b', 'c'])
|
|
249
|
+
|
|
250
|
+
FeatureKey(FeatureKey(["a", "b", "c"])) # FeatureKey copy
|
|
251
|
+
# FeatureKey(parts=['a', 'b', 'c'])
|
|
252
|
+
```
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
if TYPE_CHECKING:
|
|
256
|
+
|
|
257
|
+
@overload
|
|
258
|
+
def __init__(self, parts: str) -> None: ...
|
|
259
|
+
|
|
260
|
+
@overload
|
|
261
|
+
def __init__(self, parts: Sequence[str]) -> None: ...
|
|
262
|
+
|
|
263
|
+
@overload
|
|
264
|
+
def __init__(self, parts: FeatureKey) -> None: ...
|
|
265
|
+
|
|
266
|
+
def __init__( # pyright: ignore[reportMissingSuperCall]
|
|
267
|
+
self,
|
|
268
|
+
parts: str | Sequence[str] | FeatureKey,
|
|
269
|
+
) -> None: ...
|
|
270
|
+
|
|
271
|
+
def model_dump(self, **kwargs: Any) -> Any:
|
|
272
|
+
"""Serialize to list format for backward compatibility."""
|
|
273
|
+
# When serializing this key, return it as a list of parts
|
|
274
|
+
# instead of the full Pydantic model structure
|
|
275
|
+
return list(self.parts)
|
|
276
|
+
|
|
277
|
+
def __hash__(self) -> int:
|
|
278
|
+
"""Return hash for use as dict keys."""
|
|
279
|
+
return hash(self.parts)
|
|
280
|
+
|
|
281
|
+
def __eq__(self, other: Any) -> bool:
|
|
282
|
+
"""Check equality with another instance."""
|
|
283
|
+
if isinstance(other, self.__class__):
|
|
284
|
+
return self.parts == other.parts
|
|
285
|
+
return super().__eq__(other)
|
|
286
|
+
|
|
287
|
+
def to_column_suffix(self) -> str:
|
|
288
|
+
"""Convert to a suffix usable for database column names (typically temporary)."""
|
|
289
|
+
return "__" + "_".join(self.parts)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
class FieldKey(_Key):
|
|
293
|
+
"""
|
|
294
|
+
Field key as a sequence of string parts.
|
|
295
|
+
|
|
296
|
+
Hashable for use as dict keys in registries.
|
|
297
|
+
Parts cannot contain forward slashes (/) or double underscores (__).
|
|
298
|
+
|
|
299
|
+
Example:
|
|
300
|
+
|
|
301
|
+
```py
|
|
302
|
+
FieldKey("a/b/c") # String format
|
|
303
|
+
# FieldKey(parts=['a', 'b', 'c'])
|
|
304
|
+
|
|
305
|
+
FieldKey(["a", "b", "c"]) # List format
|
|
306
|
+
# FieldKey(parts=['a', 'b', 'c'])
|
|
307
|
+
|
|
308
|
+
FieldKey(FieldKey(["a", "b", "c"])) # FieldKey copy
|
|
309
|
+
# FieldKey(parts=['a', 'b', 'c'])
|
|
310
|
+
```
|
|
311
|
+
"""
|
|
312
|
+
|
|
313
|
+
if TYPE_CHECKING:
|
|
314
|
+
|
|
315
|
+
@overload
|
|
316
|
+
def __init__(self, parts: str) -> None: ...
|
|
317
|
+
|
|
318
|
+
@overload
|
|
319
|
+
def __init__(self, parts: Sequence[str]) -> None: ...
|
|
320
|
+
|
|
321
|
+
@overload
|
|
322
|
+
def __init__(self, parts: FieldKey) -> None: ...
|
|
323
|
+
|
|
324
|
+
def __init__( # pyright: ignore[reportMissingSuperCall]
|
|
325
|
+
self,
|
|
326
|
+
parts: str | Sequence[str] | FieldKey,
|
|
327
|
+
) -> None: ...
|
|
328
|
+
|
|
329
|
+
def model_dump(self, **kwargs: Any) -> Any:
|
|
330
|
+
"""Serialize to list format for backward compatibility."""
|
|
331
|
+
# When serializing this key, return it as a list of parts
|
|
332
|
+
# instead of the full Pydantic model structure
|
|
333
|
+
return list(self.parts)
|
|
334
|
+
|
|
335
|
+
def __hash__(self) -> int:
|
|
336
|
+
"""Return hash for use as dict keys."""
|
|
337
|
+
return hash(self.parts)
|
|
338
|
+
|
|
339
|
+
def __eq__(self, other: Any) -> bool:
|
|
340
|
+
"""Check equality with another instance."""
|
|
341
|
+
if isinstance(other, self.__class__):
|
|
342
|
+
return self.parts == other.parts
|
|
343
|
+
return super().__eq__(other)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
_CoercibleToFeatureKey: TypeAlias = _CoercibleToKey | FeatureKey
|
|
347
|
+
|
|
348
|
+
FeatureKeyAdapter = TypeAdapter(
|
|
349
|
+
FeatureKey
|
|
350
|
+
) # can call .validate_python() to transform acceptable types into a FeatureKey
|
|
351
|
+
FieldKeyAdapter = TypeAdapter(
|
|
352
|
+
FieldKey
|
|
353
|
+
) # can call .validate_python() to transform acceptable types into a FieldKey
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
def _coerce_to_feature_key(value: Any) -> FeatureKey:
|
|
357
|
+
"""Convert various types to FeatureKey.
|
|
358
|
+
|
|
359
|
+
Accepts:
|
|
360
|
+
|
|
361
|
+
- slashed `str`: `"a/b/c"`
|
|
362
|
+
|
|
363
|
+
- `Sequence[str]`: `["a", "b", "c"]`
|
|
364
|
+
|
|
365
|
+
- `FeatureKey`: pass through
|
|
366
|
+
|
|
367
|
+
- `type[BaseFeature]`: extracts .spec().key
|
|
368
|
+
|
|
369
|
+
Args:
|
|
370
|
+
value: Value to coerce to `FeatureKey`
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
canonical `FeatureKey` instance
|
|
374
|
+
|
|
375
|
+
Raises:
|
|
376
|
+
ValidationError: If value cannot be coerced to FeatureKey
|
|
377
|
+
"""
|
|
378
|
+
if isinstance(value, FeatureKey):
|
|
379
|
+
return value
|
|
380
|
+
|
|
381
|
+
# Check if it's a BaseFeature class
|
|
382
|
+
# Import here to avoid circular dependency at module level
|
|
383
|
+
from metaxy.models.feature import BaseFeature
|
|
384
|
+
|
|
385
|
+
if isinstance(value, type) and issubclass(value, BaseFeature):
|
|
386
|
+
return value.spec().key
|
|
387
|
+
|
|
388
|
+
# Handle str, Sequence[str]
|
|
389
|
+
return FeatureKeyAdapter.validate_python(value)
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
def _coerce_to_field_key(value: Any) -> FieldKey:
|
|
393
|
+
"""Convert various types to FieldKey.
|
|
394
|
+
|
|
395
|
+
Accepts:
|
|
396
|
+
|
|
397
|
+
- slashed `str`: `"a/b/c"`
|
|
398
|
+
|
|
399
|
+
- `Sequence[str]`: `["a", "b", "c"]`
|
|
400
|
+
|
|
401
|
+
- `FieldKey`: pass through
|
|
402
|
+
|
|
403
|
+
Args:
|
|
404
|
+
value: Value to coerce to `FieldKey`
|
|
405
|
+
|
|
406
|
+
Returns:
|
|
407
|
+
canonical `FieldKey` instance
|
|
408
|
+
|
|
409
|
+
Raises:
|
|
410
|
+
ValidationError: If value cannot be coerced to `FieldKey`
|
|
411
|
+
"""
|
|
412
|
+
if isinstance(value, FieldKey):
|
|
413
|
+
return value
|
|
414
|
+
|
|
415
|
+
# Handle str, Sequence[str]
|
|
416
|
+
return FieldKeyAdapter.validate_python(value)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
if TYPE_CHECKING:
|
|
420
|
+
from metaxy.models.feature import BaseFeature
|
|
421
|
+
|
|
422
|
+
# Type unions - what inputs are accepted
|
|
423
|
+
CoercibleToFeatureKey: TypeAlias = (
|
|
424
|
+
str | Sequence[str] | FeatureKey | type["BaseFeature"]
|
|
425
|
+
)
|
|
426
|
+
CoercibleToFieldKey: TypeAlias = str | Sequence[str] | FieldKey
|
|
427
|
+
|
|
428
|
+
# Annotated types for Pydantic field annotations - automatically validate
|
|
429
|
+
# After validation, these ARE FeatureKey/FieldKey (not unions)
|
|
430
|
+
ValidatedFeatureKey: TypeAlias = Annotated[
|
|
431
|
+
FeatureKey,
|
|
432
|
+
BeforeValidator(_coerce_to_feature_key),
|
|
433
|
+
Field(
|
|
434
|
+
description="Feature key. Accepts a slashed string ('a/b/c'), a sequence of strings, a FeatureKey instance, or a child class of BaseFeature"
|
|
435
|
+
),
|
|
436
|
+
]
|
|
437
|
+
|
|
438
|
+
ValidatedFieldKey: TypeAlias = Annotated[
|
|
439
|
+
FieldKey,
|
|
440
|
+
BeforeValidator(_coerce_to_field_key),
|
|
441
|
+
Field(
|
|
442
|
+
description="Field key. Accepts a slashed string ('a/b/c'), a sequence of strings, or a FieldKey instance."
|
|
443
|
+
),
|
|
444
|
+
]
|
|
445
|
+
|
|
446
|
+
# TypeAdapters for non-Pydantic usage (e.g., in metadata_store/base.py)
|
|
447
|
+
ValidatedFeatureKeyAdapter: TypeAdapter[ValidatedFeatureKey] = TypeAdapter(
|
|
448
|
+
ValidatedFeatureKey
|
|
449
|
+
)
|
|
450
|
+
ValidatedFieldKeyAdapter: TypeAdapter[ValidatedFieldKey] = TypeAdapter(
|
|
451
|
+
ValidatedFieldKey
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
|
|
455
|
+
# Collection types for common patterns - automatically validate sequences
|
|
456
|
+
# Pydantic will validate each element using ValidatedFeatureKey/ValidatedFieldKey
|
|
457
|
+
ValidatedFeatureKeySequence: TypeAlias = Annotated[
|
|
458
|
+
Sequence[ValidatedFeatureKey],
|
|
459
|
+
Field(description="Sequence items coerced into FeatureKey."),
|
|
460
|
+
]
|
|
461
|
+
|
|
462
|
+
ValidatedFieldKeySequence: TypeAlias = Annotated[
|
|
463
|
+
Sequence[ValidatedFieldKey],
|
|
464
|
+
Field(description="Sequence items coerced into FieldKey."),
|
|
465
|
+
]
|
|
466
|
+
|
|
467
|
+
# TypeAdapters for non-Pydantic usage
|
|
468
|
+
ValidatedFeatureKeySequenceAdapter: TypeAdapter[ValidatedFeatureKeySequence] = (
|
|
469
|
+
TypeAdapter(ValidatedFeatureKeySequence)
|
|
470
|
+
)
|
|
471
|
+
ValidatedFieldKeySequenceAdapter: TypeAdapter[ValidatedFieldKeySequence] = TypeAdapter(
|
|
472
|
+
ValidatedFieldKeySequence
|
|
473
|
+
)
|
|
474
|
+
|
|
475
|
+
FeatureDepMetadata: TypeAlias = dict[str, Any]
|
metaxy/py.typed
ADDED
|
File without changes
|
metaxy/utils/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Utility modules for Metaxy."""
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
class MetaxyError(Exception):
|
|
2
|
+
"""Base class for all errors thrown by the Metaxy framework.
|
|
3
|
+
|
|
4
|
+
Users should not subclass this base class for their own exceptions.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
@property
|
|
8
|
+
def is_user_code_error(self):
|
|
9
|
+
"""Returns true if this error is attributable to user code."""
|
|
10
|
+
return False
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class MetaxyInvariantViolationError(MetaxyError):
|
|
14
|
+
"""Indicates the user has violated a well-defined invariant that can only be enforced
|
|
15
|
+
at runtime.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MetaxyEmptyCodeVersionError(MetaxyInvariantViolationError):
|
|
20
|
+
"""Indicates that an empty code version was provided where it is not allowed.
|
|
21
|
+
|
|
22
|
+
Code version must be a non-empty string.
|
|
23
|
+
"""
|