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.
Files changed (111) hide show
  1. metaxy/__init__.py +170 -0
  2. metaxy/_packaging.py +96 -0
  3. metaxy/_testing/__init__.py +55 -0
  4. metaxy/_testing/config.py +43 -0
  5. metaxy/_testing/metaxy_project.py +780 -0
  6. metaxy/_testing/models.py +111 -0
  7. metaxy/_testing/parametric/__init__.py +13 -0
  8. metaxy/_testing/parametric/metadata.py +664 -0
  9. metaxy/_testing/pytest_helpers.py +74 -0
  10. metaxy/_testing/runbook.py +533 -0
  11. metaxy/_utils.py +35 -0
  12. metaxy/_version.py +1 -0
  13. metaxy/cli/app.py +97 -0
  14. metaxy/cli/console.py +13 -0
  15. metaxy/cli/context.py +167 -0
  16. metaxy/cli/graph.py +610 -0
  17. metaxy/cli/graph_diff.py +290 -0
  18. metaxy/cli/list.py +46 -0
  19. metaxy/cli/metadata.py +317 -0
  20. metaxy/cli/migrations.py +999 -0
  21. metaxy/cli/utils.py +268 -0
  22. metaxy/config.py +680 -0
  23. metaxy/entrypoints.py +296 -0
  24. metaxy/ext/__init__.py +1 -0
  25. metaxy/ext/dagster/__init__.py +54 -0
  26. metaxy/ext/dagster/constants.py +10 -0
  27. metaxy/ext/dagster/dagster_type.py +156 -0
  28. metaxy/ext/dagster/io_manager.py +200 -0
  29. metaxy/ext/dagster/metaxify.py +512 -0
  30. metaxy/ext/dagster/observable.py +115 -0
  31. metaxy/ext/dagster/resources.py +27 -0
  32. metaxy/ext/dagster/selection.py +73 -0
  33. metaxy/ext/dagster/table_metadata.py +417 -0
  34. metaxy/ext/dagster/utils.py +462 -0
  35. metaxy/ext/sqlalchemy/__init__.py +23 -0
  36. metaxy/ext/sqlalchemy/config.py +29 -0
  37. metaxy/ext/sqlalchemy/plugin.py +353 -0
  38. metaxy/ext/sqlmodel/__init__.py +13 -0
  39. metaxy/ext/sqlmodel/config.py +29 -0
  40. metaxy/ext/sqlmodel/plugin.py +499 -0
  41. metaxy/graph/__init__.py +29 -0
  42. metaxy/graph/describe.py +325 -0
  43. metaxy/graph/diff/__init__.py +21 -0
  44. metaxy/graph/diff/diff_models.py +446 -0
  45. metaxy/graph/diff/differ.py +769 -0
  46. metaxy/graph/diff/models.py +443 -0
  47. metaxy/graph/diff/rendering/__init__.py +18 -0
  48. metaxy/graph/diff/rendering/base.py +323 -0
  49. metaxy/graph/diff/rendering/cards.py +188 -0
  50. metaxy/graph/diff/rendering/formatter.py +805 -0
  51. metaxy/graph/diff/rendering/graphviz.py +246 -0
  52. metaxy/graph/diff/rendering/mermaid.py +326 -0
  53. metaxy/graph/diff/rendering/rich.py +169 -0
  54. metaxy/graph/diff/rendering/theme.py +48 -0
  55. metaxy/graph/diff/traversal.py +247 -0
  56. metaxy/graph/status.py +329 -0
  57. metaxy/graph/utils.py +58 -0
  58. metaxy/metadata_store/__init__.py +32 -0
  59. metaxy/metadata_store/_ducklake_support.py +419 -0
  60. metaxy/metadata_store/base.py +1792 -0
  61. metaxy/metadata_store/bigquery.py +354 -0
  62. metaxy/metadata_store/clickhouse.py +184 -0
  63. metaxy/metadata_store/delta.py +371 -0
  64. metaxy/metadata_store/duckdb.py +446 -0
  65. metaxy/metadata_store/exceptions.py +61 -0
  66. metaxy/metadata_store/ibis.py +542 -0
  67. metaxy/metadata_store/lancedb.py +391 -0
  68. metaxy/metadata_store/memory.py +292 -0
  69. metaxy/metadata_store/system/__init__.py +57 -0
  70. metaxy/metadata_store/system/events.py +264 -0
  71. metaxy/metadata_store/system/keys.py +9 -0
  72. metaxy/metadata_store/system/models.py +129 -0
  73. metaxy/metadata_store/system/storage.py +957 -0
  74. metaxy/metadata_store/types.py +10 -0
  75. metaxy/metadata_store/utils.py +104 -0
  76. metaxy/metadata_store/warnings.py +36 -0
  77. metaxy/migrations/__init__.py +32 -0
  78. metaxy/migrations/detector.py +291 -0
  79. metaxy/migrations/executor.py +516 -0
  80. metaxy/migrations/generator.py +319 -0
  81. metaxy/migrations/loader.py +231 -0
  82. metaxy/migrations/models.py +528 -0
  83. metaxy/migrations/ops.py +447 -0
  84. metaxy/models/__init__.py +0 -0
  85. metaxy/models/bases.py +12 -0
  86. metaxy/models/constants.py +139 -0
  87. metaxy/models/feature.py +1335 -0
  88. metaxy/models/feature_spec.py +338 -0
  89. metaxy/models/field.py +263 -0
  90. metaxy/models/fields_mapping.py +307 -0
  91. metaxy/models/filter_expression.py +297 -0
  92. metaxy/models/lineage.py +285 -0
  93. metaxy/models/plan.py +232 -0
  94. metaxy/models/types.py +475 -0
  95. metaxy/py.typed +0 -0
  96. metaxy/utils/__init__.py +1 -0
  97. metaxy/utils/constants.py +2 -0
  98. metaxy/utils/exceptions.py +23 -0
  99. metaxy/utils/hashing.py +230 -0
  100. metaxy/versioning/__init__.py +31 -0
  101. metaxy/versioning/engine.py +656 -0
  102. metaxy/versioning/feature_dep_transformer.py +151 -0
  103. metaxy/versioning/ibis.py +249 -0
  104. metaxy/versioning/lineage_handler.py +205 -0
  105. metaxy/versioning/polars.py +189 -0
  106. metaxy/versioning/renamed_df.py +35 -0
  107. metaxy/versioning/types.py +63 -0
  108. metaxy-0.0.1.dev3.dist-info/METADATA +96 -0
  109. metaxy-0.0.1.dev3.dist-info/RECORD +111 -0
  110. metaxy-0.0.1.dev3.dist-info/WHEEL +4 -0
  111. 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
@@ -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
+ ]