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
|
@@ -0,0 +1,499 @@
|
|
|
1
|
+
"""SQLModel integration for Metaxy.
|
|
2
|
+
|
|
3
|
+
This module provides a combined metaclass that allows Metaxy Feature classes
|
|
4
|
+
to also be SQLModel table classes, enabling seamless integration with SQLAlchemy/SQLModel ORMs.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import TYPE_CHECKING, Any, ClassVar
|
|
8
|
+
|
|
9
|
+
from pydantic import AwareDatetime, BaseModel
|
|
10
|
+
from sqlalchemy.types import JSON, DateTime
|
|
11
|
+
from sqlmodel import Field, SQLModel
|
|
12
|
+
from sqlmodel.main import SQLModelMetaclass
|
|
13
|
+
|
|
14
|
+
from metaxy import FeatureSpec
|
|
15
|
+
from metaxy.config import MetaxyConfig
|
|
16
|
+
from metaxy.ext.sqlmodel.config import SQLModelPluginConfig
|
|
17
|
+
from metaxy.models.constants import (
|
|
18
|
+
ALL_SYSTEM_COLUMNS,
|
|
19
|
+
METAXY_CREATED_AT,
|
|
20
|
+
METAXY_DATA_VERSION,
|
|
21
|
+
METAXY_DATA_VERSION_BY_FIELD,
|
|
22
|
+
METAXY_FEATURE_SPEC_VERSION,
|
|
23
|
+
METAXY_FEATURE_VERSION,
|
|
24
|
+
METAXY_MATERIALIZATION_ID,
|
|
25
|
+
METAXY_PROVENANCE,
|
|
26
|
+
METAXY_PROVENANCE_BY_FIELD,
|
|
27
|
+
METAXY_SNAPSHOT_VERSION,
|
|
28
|
+
SYSTEM_COLUMN_PREFIX,
|
|
29
|
+
)
|
|
30
|
+
from metaxy.models.feature import BaseFeature, FeatureGraph, MetaxyMeta
|
|
31
|
+
from metaxy.models.feature_spec import FeatureSpecWithIDColumns
|
|
32
|
+
from metaxy.models.types import ValidatedFeatureKey
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING:
|
|
35
|
+
from sqlalchemy import MetaData
|
|
36
|
+
|
|
37
|
+
from metaxy.metadata_store.ibis import IbisMetadataStore
|
|
38
|
+
|
|
39
|
+
RESERVED_SQLMODEL_FIELD_NAMES = frozenset(
|
|
40
|
+
set(ALL_SYSTEM_COLUMNS)
|
|
41
|
+
| {
|
|
42
|
+
name.removeprefix(SYSTEM_COLUMN_PREFIX)
|
|
43
|
+
for name in ALL_SYSTEM_COLUMNS
|
|
44
|
+
if name.startswith(SYSTEM_COLUMN_PREFIX)
|
|
45
|
+
}
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class MetaxyTableInfo(BaseModel):
|
|
50
|
+
feature_key: ValidatedFeatureKey
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class SQLModelFeatureMeta(MetaxyMeta, SQLModelMetaclass): # pyright: ignore[reportUnsafeMultipleInheritance]
|
|
54
|
+
def __new__(
|
|
55
|
+
cls,
|
|
56
|
+
cls_name: str,
|
|
57
|
+
bases: tuple[type[Any], ...],
|
|
58
|
+
namespace: dict[str, Any],
|
|
59
|
+
*,
|
|
60
|
+
spec: FeatureSpecWithIDColumns | None = None,
|
|
61
|
+
inject_primary_key: bool | None = None,
|
|
62
|
+
inject_index: bool | None = None,
|
|
63
|
+
**kwargs: Any,
|
|
64
|
+
) -> type[Any]:
|
|
65
|
+
"""Create a new SQLModel + Metaxy Feature class.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
cls_name: Name of the class being created
|
|
69
|
+
bases: Base classes
|
|
70
|
+
namespace: Class namespace (attributes and methods)
|
|
71
|
+
spec: Metaxy FeatureSpec (required for concrete features)
|
|
72
|
+
inject_primary_key: If True, automatically create composite primary key
|
|
73
|
+
including id_columns + (metaxy_created_at, metaxy_data_version).
|
|
74
|
+
inject_index: If True, automatically create composite index
|
|
75
|
+
including id_columns + (metaxy_created_at, metaxy_data_version).
|
|
76
|
+
**kwargs: Additional keyword arguments (e.g., table=True for SQLModel)
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
New class that is both a SQLModel table and a Metaxy feature
|
|
80
|
+
"""
|
|
81
|
+
# Override frozen config for SQLModel - instances need to be mutable for ORM
|
|
82
|
+
if "model_config" not in namespace:
|
|
83
|
+
from pydantic import ConfigDict
|
|
84
|
+
|
|
85
|
+
namespace["model_config"] = ConfigDict(frozen=False)
|
|
86
|
+
|
|
87
|
+
# Check plugin config for defaults
|
|
88
|
+
sqlmodel_config = MetaxyConfig.get_plugin("sqlmodel", SQLModelPluginConfig)
|
|
89
|
+
if inject_primary_key is None:
|
|
90
|
+
inject_primary_key = sqlmodel_config.inject_primary_key
|
|
91
|
+
if inject_index is None:
|
|
92
|
+
inject_index = sqlmodel_config.inject_index
|
|
93
|
+
|
|
94
|
+
# If this is a concrete table (table=True) with a spec
|
|
95
|
+
if kwargs.get("table") and spec is not None:
|
|
96
|
+
# Forbid custom __tablename__ since it won't work with metadata store's get_table_name()
|
|
97
|
+
if "__tablename__" in namespace:
|
|
98
|
+
raise ValueError(
|
|
99
|
+
f"Cannot define custom __tablename__ in {cls_name}. "
|
|
100
|
+
"The table name is automatically derived from the feature key. "
|
|
101
|
+
"If you need a different table name, adjust the feature key instead."
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
# Prevent user-defined fields from shadowing system-managed columns
|
|
105
|
+
conflicts = {
|
|
106
|
+
attr_name
|
|
107
|
+
for attr_name in namespace
|
|
108
|
+
if attr_name in RESERVED_SQLMODEL_FIELD_NAMES
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
# Also guard against explicit sa_column_kwargs targeting system columns
|
|
112
|
+
for attr_name, attr_value in namespace.items():
|
|
113
|
+
sa_column_kwargs = getattr(attr_value, "sa_column_kwargs", None)
|
|
114
|
+
if isinstance(sa_column_kwargs, dict):
|
|
115
|
+
column_name = sa_column_kwargs.get("name")
|
|
116
|
+
if column_name in ALL_SYSTEM_COLUMNS:
|
|
117
|
+
conflicts.add(attr_name)
|
|
118
|
+
|
|
119
|
+
if conflicts:
|
|
120
|
+
reserved = ", ".join(sorted(ALL_SYSTEM_COLUMNS))
|
|
121
|
+
conflict_list = ", ".join(sorted(conflicts))
|
|
122
|
+
raise ValueError(
|
|
123
|
+
"Cannot define SQLModel field(s) "
|
|
124
|
+
f"{conflict_list} because they map to reserved Metaxy system columns. "
|
|
125
|
+
f"Reserved columns: {reserved}"
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# Automatically set __tablename__ from the feature key
|
|
129
|
+
namespace["__tablename__"] = spec.key.table_name
|
|
130
|
+
|
|
131
|
+
# Inject table args (info metadata + optional constraints)
|
|
132
|
+
cls._inject_table_args(
|
|
133
|
+
namespace, spec, cls_name, inject_primary_key, inject_index
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
# Call super().__new__ which follows MRO: MetaxyMeta -> SQLModelMetaclass -> ...
|
|
137
|
+
# MetaxyMeta will consume the spec parameter and pass remaining kwargs to SQLModelMetaclass
|
|
138
|
+
new_class = super().__new__(
|
|
139
|
+
cls, cls_name, bases, namespace, spec=spec, **kwargs
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
return new_class
|
|
143
|
+
|
|
144
|
+
@staticmethod
|
|
145
|
+
def _inject_table_args(
|
|
146
|
+
namespace: dict[str, Any],
|
|
147
|
+
spec: FeatureSpec,
|
|
148
|
+
cls_name: str,
|
|
149
|
+
inject_primary_key: bool,
|
|
150
|
+
inject_index: bool,
|
|
151
|
+
) -> None:
|
|
152
|
+
"""Inject Metaxy table args (info metadata + optional constraints) via __table_args__.
|
|
153
|
+
|
|
154
|
+
This method handles:
|
|
155
|
+
|
|
156
|
+
1. Always injects info metadata with feature key for efficient lookup
|
|
157
|
+
|
|
158
|
+
2. Optionally injects composite primary key and/or index constraints
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
namespace: Class namespace to modify
|
|
162
|
+
spec: Feature specification with key and id_columns
|
|
163
|
+
cls_name: Name of the class being created
|
|
164
|
+
inject_primary_key: If True, inject composite primary key
|
|
165
|
+
inject_index: If True, inject composite index
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
from sqlalchemy import Index, PrimaryKeyConstraint
|
|
169
|
+
|
|
170
|
+
# Prepare info dict with Metaxy metadata (always added)
|
|
171
|
+
metaxy_info = {
|
|
172
|
+
"metaxy-system": MetaxyTableInfo(feature_key=spec.key).model_dump()
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
# Prepare constraints if requested
|
|
176
|
+
constraints = []
|
|
177
|
+
if inject_primary_key or inject_index:
|
|
178
|
+
# Composite key/index columns: id_columns + metaxy_created_at + metaxy_data_version
|
|
179
|
+
key_columns = list(spec.id_columns) + [
|
|
180
|
+
METAXY_CREATED_AT,
|
|
181
|
+
METAXY_DATA_VERSION,
|
|
182
|
+
]
|
|
183
|
+
|
|
184
|
+
if inject_primary_key:
|
|
185
|
+
pk_constraint = PrimaryKeyConstraint(*key_columns, name="metaxy_pk")
|
|
186
|
+
constraints.append(pk_constraint)
|
|
187
|
+
|
|
188
|
+
if inject_index:
|
|
189
|
+
idx = Index("metaxy_idx", *key_columns)
|
|
190
|
+
constraints.append(idx)
|
|
191
|
+
|
|
192
|
+
# Merge with existing __table_args__
|
|
193
|
+
if "__table_args__" in namespace:
|
|
194
|
+
existing_args = namespace["__table_args__"]
|
|
195
|
+
|
|
196
|
+
if isinstance(existing_args, dict):
|
|
197
|
+
# Dict format: merge info, convert to tuple if we have constraints
|
|
198
|
+
existing_info = existing_args.get("info", {})
|
|
199
|
+
existing_info.update(metaxy_info)
|
|
200
|
+
existing_args["info"] = existing_info
|
|
201
|
+
|
|
202
|
+
if constraints:
|
|
203
|
+
# Convert to tuple format with constraints
|
|
204
|
+
namespace["__table_args__"] = tuple(constraints) + (existing_args,)
|
|
205
|
+
# else: keep as dict
|
|
206
|
+
|
|
207
|
+
elif isinstance(existing_args, tuple):
|
|
208
|
+
# Tuple format: append constraints and merge info in table kwargs dict
|
|
209
|
+
# Extract existing constraints and table kwargs
|
|
210
|
+
if existing_args and isinstance(existing_args[-1], dict):
|
|
211
|
+
# Has table kwargs dict at the end
|
|
212
|
+
existing_constraints = existing_args[:-1]
|
|
213
|
+
table_kwargs = dict(existing_args[-1])
|
|
214
|
+
else:
|
|
215
|
+
# No table kwargs dict
|
|
216
|
+
existing_constraints = existing_args
|
|
217
|
+
table_kwargs = {}
|
|
218
|
+
|
|
219
|
+
# Merge info
|
|
220
|
+
existing_info = table_kwargs.get("info", {})
|
|
221
|
+
existing_info.update(metaxy_info)
|
|
222
|
+
table_kwargs["info"] = existing_info
|
|
223
|
+
|
|
224
|
+
# Combine: existing constraints + new constraints + table kwargs
|
|
225
|
+
namespace["__table_args__"] = (
|
|
226
|
+
existing_constraints + tuple(constraints) + (table_kwargs,)
|
|
227
|
+
)
|
|
228
|
+
else:
|
|
229
|
+
raise ValueError(
|
|
230
|
+
f"Invalid __table_args__ type in {cls_name}: {type(existing_args)}"
|
|
231
|
+
)
|
|
232
|
+
else:
|
|
233
|
+
# No existing __table_args__
|
|
234
|
+
if constraints:
|
|
235
|
+
# Create tuple format with constraints + info
|
|
236
|
+
namespace["__table_args__"] = tuple(constraints) + (
|
|
237
|
+
{"info": metaxy_info},
|
|
238
|
+
)
|
|
239
|
+
else:
|
|
240
|
+
# Just info, use dict format
|
|
241
|
+
namespace["__table_args__"] = {"info": metaxy_info}
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
class BaseSQLModelFeature( # pyright: ignore[reportIncompatibleMethodOverride, reportUnsafeMultipleInheritance]
|
|
245
|
+
SQLModel, BaseFeature, metaclass=SQLModelFeatureMeta, spec=None
|
|
246
|
+
): # type: ignore[misc]
|
|
247
|
+
"""Base class for `Metaxy` features that are also `SQLModel` tables.
|
|
248
|
+
|
|
249
|
+
!!! example
|
|
250
|
+
|
|
251
|
+
```py
|
|
252
|
+
from metaxy.integrations.sqlmodel import BaseSQLModelFeature
|
|
253
|
+
from metaxy import FeatureSpec, FeatureKey, FieldSpec, FieldKey
|
|
254
|
+
from sqlmodel import Field
|
|
255
|
+
|
|
256
|
+
class VideoFeature(
|
|
257
|
+
BaseSQLModelFeature,
|
|
258
|
+
table=True,
|
|
259
|
+
spec=FeatureSpec(
|
|
260
|
+
key=FeatureKey(["video"]),
|
|
261
|
+
id_columns=["uid"],
|
|
262
|
+
fields=[
|
|
263
|
+
FieldSpec(
|
|
264
|
+
key=FieldKey(["video_file"]),
|
|
265
|
+
code_version="1",
|
|
266
|
+
),
|
|
267
|
+
],
|
|
268
|
+
),
|
|
269
|
+
):
|
|
270
|
+
|
|
271
|
+
uid: str = Field(primary_key=True)
|
|
272
|
+
path: str
|
|
273
|
+
duration: float
|
|
274
|
+
|
|
275
|
+
# Now you can use both Metaxy and SQLModel features:
|
|
276
|
+
# - VideoFeature.feature_version() -> Metaxy versioning
|
|
277
|
+
# - session.exec(select(VideoFeature)) -> SQLModel queries
|
|
278
|
+
```
|
|
279
|
+
"""
|
|
280
|
+
|
|
281
|
+
# Override the frozen config from Feature's FrozenBaseModel
|
|
282
|
+
# SQLModel instances need to be mutable for ORM operations
|
|
283
|
+
model_config = {"frozen": False} # pyright: ignore[reportAssignmentType]
|
|
284
|
+
|
|
285
|
+
# Re-declare ClassVar attributes from BaseFeature for type checker visibility.
|
|
286
|
+
# These are set by MetaxyMeta at class creation time but type checkers can't see them
|
|
287
|
+
# through the complex metaclass inheritance chain.
|
|
288
|
+
_spec: ClassVar[FeatureSpec]
|
|
289
|
+
graph: ClassVar[FeatureGraph]
|
|
290
|
+
project: ClassVar[str]
|
|
291
|
+
|
|
292
|
+
# Using sa_column_kwargs to map to the actual column names used by Metaxy
|
|
293
|
+
# Descriptions match those in BaseFeature for consistency in Dagster UI
|
|
294
|
+
metaxy_provenance: str | None = Field(
|
|
295
|
+
default=None,
|
|
296
|
+
description="Hash of metaxy_provenance_by_field",
|
|
297
|
+
sa_column_kwargs={
|
|
298
|
+
"name": METAXY_PROVENANCE,
|
|
299
|
+
},
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
metaxy_provenance_by_field: dict[str, str] = Field(
|
|
303
|
+
default=None,
|
|
304
|
+
description="Field-level provenance hashes (maps field names to hashes)",
|
|
305
|
+
sa_type=JSON,
|
|
306
|
+
sa_column_kwargs={
|
|
307
|
+
"name": METAXY_PROVENANCE_BY_FIELD,
|
|
308
|
+
},
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
metaxy_feature_version: str | None = Field(
|
|
312
|
+
default=None,
|
|
313
|
+
description="Hash of the feature definition (dependencies + fields + code_versions)",
|
|
314
|
+
sa_column_kwargs={
|
|
315
|
+
"name": METAXY_FEATURE_VERSION,
|
|
316
|
+
},
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
metaxy_feature_spec_version: str | None = Field(
|
|
320
|
+
default=None,
|
|
321
|
+
description="Hash of the complete feature specification.",
|
|
322
|
+
sa_column_kwargs={
|
|
323
|
+
"name": METAXY_FEATURE_SPEC_VERSION,
|
|
324
|
+
},
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
metaxy_snapshot_version: str | None = Field(
|
|
328
|
+
default=None,
|
|
329
|
+
description="Hash of the entire feature graph snapshot",
|
|
330
|
+
sa_column_kwargs={
|
|
331
|
+
"name": METAXY_SNAPSHOT_VERSION,
|
|
332
|
+
},
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
metaxy_data_version: str | None = Field(
|
|
336
|
+
default=None,
|
|
337
|
+
description="Hash of metaxy_data_version_by_field",
|
|
338
|
+
sa_column_kwargs={
|
|
339
|
+
"name": METAXY_DATA_VERSION,
|
|
340
|
+
},
|
|
341
|
+
)
|
|
342
|
+
|
|
343
|
+
metaxy_data_version_by_field: dict[str, str] | None = Field(
|
|
344
|
+
default=None,
|
|
345
|
+
description="Field-level data version hashes (maps field names to version hashes)",
|
|
346
|
+
sa_type=JSON,
|
|
347
|
+
sa_column_kwargs={
|
|
348
|
+
"name": METAXY_DATA_VERSION_BY_FIELD,
|
|
349
|
+
},
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
metaxy_created_at: AwareDatetime | None = Field(
|
|
353
|
+
default=None,
|
|
354
|
+
description="Timestamp when the metadata row was created (UTC)",
|
|
355
|
+
sa_type=DateTime(timezone=True),
|
|
356
|
+
sa_column_kwargs={
|
|
357
|
+
"name": METAXY_CREATED_AT,
|
|
358
|
+
},
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
metaxy_materialization_id: str | None = Field(
|
|
362
|
+
default=None,
|
|
363
|
+
description="External orchestration run ID (e.g., Dagster Run ID)",
|
|
364
|
+
sa_column_kwargs={
|
|
365
|
+
"name": METAXY_MATERIALIZATION_ID,
|
|
366
|
+
},
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
|
|
370
|
+
# Convenience wrappers for filtering SQLModel metadata
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
def filter_feature_sqlmodel_metadata(
|
|
374
|
+
store: "IbisMetadataStore",
|
|
375
|
+
source_metadata: "MetaData",
|
|
376
|
+
project: str | None = None,
|
|
377
|
+
filter_by_project: bool = True,
|
|
378
|
+
inject_primary_key: bool | None = None,
|
|
379
|
+
inject_index: bool | None = None,
|
|
380
|
+
) -> tuple[str, "MetaData"]:
|
|
381
|
+
"""Get SQLAlchemy URL and filtered SQLModel feature metadata for a metadata store.
|
|
382
|
+
|
|
383
|
+
This function transforms SQLModel table names to include the store's table_prefix,
|
|
384
|
+
ensuring that table names in the metadata match what's expected in the database.
|
|
385
|
+
|
|
386
|
+
You can pass `SQLModel.metadata` directly - this function will transform table names
|
|
387
|
+
by adding the store's `table_prefix`. The returned metadata will have prefixed table
|
|
388
|
+
names that match the actual database tables.
|
|
389
|
+
|
|
390
|
+
This function must be called after init_metaxy() to ensure features are loaded.
|
|
391
|
+
|
|
392
|
+
Args:
|
|
393
|
+
store: IbisMetadataStore instance (provides table_prefix and sqlalchemy_url)
|
|
394
|
+
source_metadata: Source SQLAlchemy MetaData to filter (typically SQLModel.metadata).
|
|
395
|
+
Tables are looked up in this metadata by their unprefixed names.
|
|
396
|
+
project: Project name to filter by. If None, uses MetaxyConfig.get().project
|
|
397
|
+
filter_by_project: If True, only include features for the specified project.
|
|
398
|
+
inject_primary_key: If True, inject composite primary key constraints.
|
|
399
|
+
If False, do not inject. If None, uses config default.
|
|
400
|
+
inject_index: If True, inject composite index.
|
|
401
|
+
If False, do not inject. If None, uses config default.
|
|
402
|
+
|
|
403
|
+
Returns:
|
|
404
|
+
Tuple of (sqlalchemy_url, filtered_metadata)
|
|
405
|
+
|
|
406
|
+
Raises:
|
|
407
|
+
ValueError: If store's sqlalchemy_url is empty
|
|
408
|
+
|
|
409
|
+
Example:
|
|
410
|
+
|
|
411
|
+
```py
|
|
412
|
+
from sqlmodel import SQLModel
|
|
413
|
+
from metaxy.ext.sqlmodel import filter_feature_sqlmodel_metadata
|
|
414
|
+
from metaxy import init_metaxy
|
|
415
|
+
from metaxy.config import MetaxyConfig
|
|
416
|
+
|
|
417
|
+
# Load features first
|
|
418
|
+
init_metaxy()
|
|
419
|
+
|
|
420
|
+
# Get store instance
|
|
421
|
+
config = MetaxyConfig.get()
|
|
422
|
+
store = config.get_store("my_store")
|
|
423
|
+
|
|
424
|
+
# Filter SQLModel metadata with prefix transformation
|
|
425
|
+
url, metadata = filter_feature_sqlmodel_metadata(store, SQLModel.metadata)
|
|
426
|
+
|
|
427
|
+
# Use with Alembic env.py
|
|
428
|
+
from alembic import context
|
|
429
|
+
url, target_metadata = filter_feature_sqlmodel_metadata(store, SQLModel.metadata)
|
|
430
|
+
context.configure(url=url, target_metadata=target_metadata)
|
|
431
|
+
```
|
|
432
|
+
"""
|
|
433
|
+
|
|
434
|
+
from sqlalchemy import MetaData
|
|
435
|
+
|
|
436
|
+
config = MetaxyConfig.get()
|
|
437
|
+
|
|
438
|
+
if project is None:
|
|
439
|
+
project = config.project
|
|
440
|
+
|
|
441
|
+
# Check plugin config for defaults
|
|
442
|
+
sqlmodel_config = config.get_plugin("sqlmodel", SQLModelPluginConfig)
|
|
443
|
+
if inject_primary_key is None:
|
|
444
|
+
inject_primary_key = sqlmodel_config.inject_primary_key
|
|
445
|
+
if inject_index is None:
|
|
446
|
+
inject_index = sqlmodel_config.inject_index
|
|
447
|
+
|
|
448
|
+
# Get SQLAlchemy URL from store
|
|
449
|
+
if not store.sqlalchemy_url:
|
|
450
|
+
raise ValueError("IbisMetadataStore has an empty `sqlalchemy_url`.")
|
|
451
|
+
url = store.sqlalchemy_url
|
|
452
|
+
|
|
453
|
+
# Create new metadata with transformed table names
|
|
454
|
+
filtered_metadata = MetaData()
|
|
455
|
+
|
|
456
|
+
# Get the FeatureGraph to look up feature classes by key
|
|
457
|
+
from metaxy.models.feature import FeatureGraph
|
|
458
|
+
|
|
459
|
+
feature_graph = FeatureGraph.get_active()
|
|
460
|
+
|
|
461
|
+
# Iterate over tables in source metadata
|
|
462
|
+
for table_name, original_table in source_metadata.tables.items():
|
|
463
|
+
# Check if this table has Metaxy feature metadata
|
|
464
|
+
if metaxy_system_info := original_table.info.get("metaxy-system"):
|
|
465
|
+
metaxy_info = MetaxyTableInfo.model_validate(metaxy_system_info)
|
|
466
|
+
feature_key = metaxy_info.feature_key
|
|
467
|
+
else:
|
|
468
|
+
continue
|
|
469
|
+
# Look up the feature class from the FeatureGraph
|
|
470
|
+
feature_cls = feature_graph.features_by_key.get(feature_key)
|
|
471
|
+
if feature_cls is None:
|
|
472
|
+
# Skip tables for features that aren't registered
|
|
473
|
+
continue
|
|
474
|
+
|
|
475
|
+
# Filter by project if requested
|
|
476
|
+
if filter_by_project:
|
|
477
|
+
feature_project = getattr(feature_cls, "project", None)
|
|
478
|
+
if feature_project != project:
|
|
479
|
+
continue
|
|
480
|
+
|
|
481
|
+
# Compute prefixed name using store's table_prefix
|
|
482
|
+
prefixed_name = store.get_table_name(feature_key)
|
|
483
|
+
|
|
484
|
+
# Copy table to new metadata with prefixed name
|
|
485
|
+
new_table = original_table.to_metadata(filtered_metadata, name=prefixed_name)
|
|
486
|
+
|
|
487
|
+
# Inject constraints if requested
|
|
488
|
+
if inject_primary_key or inject_index:
|
|
489
|
+
from metaxy.ext.sqlalchemy.plugin import _inject_constraints
|
|
490
|
+
|
|
491
|
+
spec = feature_cls.spec()
|
|
492
|
+
_inject_constraints(
|
|
493
|
+
table=new_table,
|
|
494
|
+
spec=spec,
|
|
495
|
+
inject_primary_key=inject_primary_key,
|
|
496
|
+
inject_index=inject_index,
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
return url, filtered_metadata
|
metaxy/graph/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Graph visualization and rendering utilities."""
|
|
2
|
+
|
|
3
|
+
from metaxy.graph.describe import (
|
|
4
|
+
describe_graph,
|
|
5
|
+
get_feature_dependencies,
|
|
6
|
+
get_feature_dependents,
|
|
7
|
+
)
|
|
8
|
+
from metaxy.graph.diff import GraphData
|
|
9
|
+
from metaxy.graph.diff.rendering import (
|
|
10
|
+
BaseRenderer,
|
|
11
|
+
CardsRenderer,
|
|
12
|
+
GraphvizRenderer,
|
|
13
|
+
MermaidRenderer,
|
|
14
|
+
RenderConfig,
|
|
15
|
+
TerminalRenderer,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"BaseRenderer",
|
|
20
|
+
"RenderConfig",
|
|
21
|
+
"GraphData",
|
|
22
|
+
"TerminalRenderer",
|
|
23
|
+
"CardsRenderer",
|
|
24
|
+
"MermaidRenderer",
|
|
25
|
+
"GraphvizRenderer",
|
|
26
|
+
"describe_graph",
|
|
27
|
+
"get_feature_dependencies",
|
|
28
|
+
"get_feature_dependents",
|
|
29
|
+
]
|