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,353 @@
1
+ """SQLAlchemy integration plugin for metaxy.
2
+
3
+ This module provides SQLAlchemy Table definitions and helpers for metaxy system tables
4
+ and user-defined feature tables. These can be used with migration tools like Alembic.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from typing import TYPE_CHECKING
10
+
11
+ from sqlalchemy import Column, DateTime, Index, MetaData, String, Table
12
+
13
+ from metaxy.config import MetaxyConfig
14
+ from metaxy.ext.sqlalchemy.config import SQLAlchemyConfig
15
+ from metaxy.metadata_store.system import EVENTS_KEY, FEATURE_VERSIONS_KEY
16
+ from metaxy.models.constants import (
17
+ METAXY_FEATURE_SPEC_VERSION,
18
+ METAXY_FEATURE_VERSION,
19
+ METAXY_FULL_DEFINITION_VERSION,
20
+ METAXY_SNAPSHOT_VERSION,
21
+ )
22
+ from metaxy.models.feature_spec import FeatureSpec
23
+
24
+ if TYPE_CHECKING:
25
+ from metaxy.metadata_store.ibis import IbisMetadataStore
26
+
27
+
28
+ # System Tables
29
+
30
+
31
+ def create_system_tables(
32
+ metadata: MetaData,
33
+ table_prefix: str = "",
34
+ ) -> tuple[Table, Table]:
35
+ """Create system table definitions in the given metadata.
36
+
37
+ System tables always include primary key constraints since they are controlled by metaxy.
38
+
39
+ Args:
40
+ metadata: SQLAlchemy MetaData object to add tables to
41
+ table_prefix: Optional prefix to prepend to table names (e.g., "dev_")
42
+
43
+ Returns:
44
+ Tuple of (feature_versions_table, events_table)
45
+ """
46
+ feature_versions_name = (
47
+ f"{table_prefix}{FEATURE_VERSIONS_KEY.table_name}"
48
+ if table_prefix
49
+ else FEATURE_VERSIONS_KEY.table_name
50
+ )
51
+
52
+ feature_versions_table = Table(
53
+ feature_versions_name,
54
+ metadata,
55
+ # Composite primary key
56
+ Column("project", String, primary_key=True, index=True),
57
+ Column("feature_key", String, primary_key=True, index=True),
58
+ Column(
59
+ METAXY_FEATURE_SPEC_VERSION,
60
+ String,
61
+ primary_key=True,
62
+ ),
63
+ # Versioning columns
64
+ Column(METAXY_FEATURE_VERSION, String, index=True),
65
+ Column(METAXY_FULL_DEFINITION_VERSION, String, index=True),
66
+ Column(METAXY_SNAPSHOT_VERSION, String, index=True),
67
+ # Metadata columns
68
+ Column("recorded_at", DateTime, index=True),
69
+ Column("feature_schema", String), # JSON string
70
+ Column("tags", String, default="{}"), # JSON string
71
+ # Additional indexes
72
+ Index(
73
+ f"idx_{feature_versions_name}_lookup",
74
+ "project",
75
+ "feature_key",
76
+ METAXY_FEATURE_VERSION,
77
+ ),
78
+ )
79
+
80
+ events_name = (
81
+ f"{table_prefix}{EVENTS_KEY.table_name}"
82
+ if table_prefix
83
+ else EVENTS_KEY.table_name
84
+ )
85
+
86
+ events_table = Table(
87
+ events_name,
88
+ metadata,
89
+ # Composite primary key matching Polars append-only storage
90
+ Column("project", String, primary_key=True, index=True),
91
+ Column("execution_id", String, primary_key=True, index=True),
92
+ Column("timestamp", DateTime, primary_key=True),
93
+ # Event fields
94
+ Column("event_type", String, index=True),
95
+ Column("feature_key", String, nullable=True, index=True),
96
+ Column("payload", String, default=""), # JSON string
97
+ # Additional indexes
98
+ Index(
99
+ f"idx_{events_name}_lookup",
100
+ "project",
101
+ "execution_id",
102
+ "event_type",
103
+ ),
104
+ )
105
+
106
+ return feature_versions_table, events_table
107
+
108
+
109
+ def _get_store_sqlalchemy_url(store: IbisMetadataStore) -> str:
110
+ """Get SQLAlchemy URL from an IbisMetadataStore instance.
111
+
112
+ Args:
113
+ store: IbisMetadataStore instance
114
+
115
+ Returns:
116
+ SQLAlchemy connection URL string
117
+
118
+ Raises:
119
+ ValueError: If sqlalchemy_url is empty
120
+ """
121
+ if not store.sqlalchemy_url:
122
+ raise ValueError("IbisMetadataStore has an empty `sqlalchemy_url`.")
123
+
124
+ return store.sqlalchemy_url
125
+
126
+
127
+ def _get_system_metadata(
128
+ table_prefix: str = "",
129
+ ) -> MetaData:
130
+ """Create SQLAlchemy metadata containing system tables.
131
+
132
+ System tables always include primary key constraints.
133
+
134
+ Args:
135
+ table_prefix: Optional prefix to prepend to table names
136
+
137
+ Returns:
138
+ MetaData containing system table definitions
139
+ """
140
+ metadata = MetaData()
141
+ create_system_tables(metadata, table_prefix=table_prefix)
142
+ return metadata
143
+
144
+
145
+ def get_system_slqa_metadata(
146
+ store: IbisMetadataStore,
147
+ ) -> tuple[str, MetaData]:
148
+ """Get SQLAlchemy URL and Metaxy system tables metadata for a metadata store.
149
+
150
+ This function retrieves both the connection URL and system table metadata
151
+ for a store, with the store's `table_prefix` automatically applied to table names.
152
+
153
+ Args:
154
+ store: IbisMetadataStore instance
155
+
156
+ Returns:
157
+ Tuple of (sqlalchemy_url, system_metadata)
158
+
159
+ Raises:
160
+ ValueError: If store's sqlalchemy_url is empty
161
+ """
162
+ url = _get_store_sqlalchemy_url(store)
163
+ metadata = _get_system_metadata(table_prefix=store._table_prefix)
164
+ return url, metadata
165
+
166
+
167
+ def _get_features_metadata(
168
+ source_metadata: MetaData,
169
+ store: IbisMetadataStore,
170
+ project: str | None = None,
171
+ filter_by_project: bool = True,
172
+ inject_primary_key: bool | None = None,
173
+ inject_index: bool | None = None,
174
+ ) -> MetaData:
175
+ """Filter user-defined feature tables from source metadata by project.
176
+
177
+ This function must be called after init_metaxy() to ensure features are loaded.
178
+
179
+ Args:
180
+ source_metadata: Source SQLAlchemy MetaData to filter (e.g., SQLModel.metadata)
181
+ store: IbisMetadataStore instance (used to get table_prefix)
182
+ project: Project name to filter by. If None, uses MetaxyConfig.get().project
183
+ filter_by_project: If True, only include features for the specified project.
184
+ inject_primary_key: If True, inject composite primary key constraints.
185
+ If False, do not inject. If None, uses config default.
186
+ inject_index: If True, inject composite index.
187
+ If False, do not inject. If None, uses config default.
188
+
189
+ Returns:
190
+ Filtered SQLAlchemy MetaData containing only project-scoped feature tables
191
+ """
192
+ from metaxy.models.feature import FeatureGraph
193
+
194
+ config = MetaxyConfig.get()
195
+
196
+ if project is None:
197
+ project = config.project
198
+
199
+ # Check plugin config for defaults
200
+ sqlalchemy_config = config.get_plugin("sqlalchemy", SQLAlchemyConfig)
201
+ if inject_primary_key is None:
202
+ inject_primary_key = sqlalchemy_config.inject_primary_key
203
+ if inject_index is None:
204
+ inject_index = sqlalchemy_config.inject_index
205
+
206
+ # Get the active feature graph
207
+ graph = FeatureGraph.get_active()
208
+
209
+ # Compute expected table names for features in the project
210
+ expected_table_names = set()
211
+ feature_specs_by_table_name = {}
212
+
213
+ for feature_key, feature_cls in graph.features_by_key.items():
214
+ # Filter by project if requested
215
+ if filter_by_project and hasattr(feature_cls, "project"):
216
+ feature_project = getattr(feature_cls, "project")
217
+ if feature_project != project:
218
+ continue
219
+
220
+ table_name = store.get_table_name(feature_key)
221
+
222
+ expected_table_names.add(table_name)
223
+ feature_specs_by_table_name[table_name] = feature_cls.spec()
224
+
225
+ # Filter source metadata to only include expected tables
226
+ filtered_metadata = MetaData()
227
+
228
+ for table_name, table in source_metadata.tables.items():
229
+ if table_name in expected_table_names:
230
+ # Copy table to filtered metadata
231
+ new_table = table.to_metadata(filtered_metadata)
232
+
233
+ # Inject constraints if requested
234
+ spec = feature_specs_by_table_name[table_name]
235
+ _inject_constraints(
236
+ table=new_table,
237
+ spec=spec,
238
+ inject_primary_key=inject_primary_key,
239
+ inject_index=inject_index,
240
+ )
241
+
242
+ return filtered_metadata
243
+
244
+
245
+ def _inject_constraints(
246
+ table: Table,
247
+ spec: FeatureSpec,
248
+ inject_primary_key: bool,
249
+ inject_index: bool,
250
+ ) -> None:
251
+ """Inject primary key and/or index constraints on a table.
252
+
253
+ Args:
254
+ table: SQLAlchemy Table to modify
255
+ spec: Feature specification with id_columns
256
+ inject_primary_key: If True, inject composite primary key
257
+ inject_index: If True, inject composite index
258
+ """
259
+ from sqlalchemy import PrimaryKeyConstraint
260
+
261
+ from metaxy.models.constants import METAXY_CREATED_AT, METAXY_DATA_VERSION
262
+
263
+ # Composite key/index columns: id_columns + metaxy_created_at + metaxy_data_version
264
+ key_columns = list(spec.id_columns) + [METAXY_CREATED_AT, METAXY_DATA_VERSION]
265
+
266
+ if inject_primary_key:
267
+ # Add primary key constraint
268
+ pk_constraint = PrimaryKeyConstraint(*key_columns, name="metaxy_pk")
269
+ table.append_constraint(pk_constraint)
270
+
271
+ if inject_index:
272
+ # Add composite index
273
+ idx = Index(
274
+ "metaxy_idx",
275
+ *key_columns,
276
+ )
277
+ table.append_constraint(idx)
278
+
279
+
280
+ def filter_feature_sqla_metadata(
281
+ store: IbisMetadataStore,
282
+ source_metadata: MetaData,
283
+ project: str | None = None,
284
+ filter_by_project: bool = True,
285
+ inject_primary_key: bool | None = None,
286
+ inject_index: bool | None = None,
287
+ ) -> tuple[str, MetaData]:
288
+ """Get SQLAlchemy URL and feature table metadata for a metadata store.
289
+
290
+ This function filters the source metadata to include only feature tables
291
+ belonging to the specified project, and returns the connection URL for the store.
292
+
293
+ This function must be called after init_metaxy() to ensure features are loaded.
294
+
295
+ Args:
296
+ store: IbisMetadataStore instance
297
+ source_metadata: Source SQLAlchemy MetaData to filter.
298
+ project: Project name to filter by. If None, uses MetaxyConfig.get().project
299
+ filter_by_project: If True, only include features for the specified project.
300
+ If False, include all features.
301
+ inject_primary_key: If True, inject composite primary key constraints.
302
+ If False, do not inject. If None, uses config default.
303
+ inject_index: If True, inject composite index.
304
+ If False, do not inject. If None, uses config default.
305
+
306
+ Returns:
307
+ Tuple of (sqlalchemy_url, filtered_metadata)
308
+
309
+ Raises:
310
+ ValueError: If store's sqlalchemy_url is empty
311
+ ImportError: If source_metadata is None and SQLModel is not installed
312
+
313
+ Example: Basic Usage
314
+
315
+ ```py
316
+ from metaxy.ext.sqlalchemy import filter_feature_sqla_metadata
317
+ from metaxy import init_metaxy
318
+ from metaxy.config import MetaxyConfig
319
+
320
+ # Load features first
321
+ init_metaxy()
322
+
323
+ # Get store instance
324
+ config = MetaxyConfig.get()
325
+ store = config.get_store("my_store")
326
+
327
+ # With custom metadata
328
+ from sqlalchemy import MetaData
329
+ my_metadata = MetaData()
330
+ # ... define tables in my_metadata ...
331
+
332
+ # apply the filter function
333
+ url, metadata = filter_feature_sqla_metadata(store, source_metadata=my_metadata)
334
+ ```
335
+
336
+ Example: With SQLModel
337
+
338
+ ```py
339
+ # With SQLModel
340
+ from sqlmodel import SQLModel
341
+ url, metadata = filter_feature_sqla_metadata(store, SQLModel.metadata)
342
+ ```
343
+ """
344
+ url = _get_store_sqlalchemy_url(store)
345
+ metadata = _get_features_metadata(
346
+ source_metadata=source_metadata,
347
+ store=store,
348
+ project=project,
349
+ filter_by_project=filter_by_project,
350
+ inject_primary_key=inject_primary_key,
351
+ inject_index=inject_index,
352
+ )
353
+ return url, metadata
@@ -0,0 +1,13 @@
1
+ from metaxy.ext.sqlmodel.config import SQLModelPluginConfig
2
+ from metaxy.ext.sqlmodel.plugin import (
3
+ BaseSQLModelFeature,
4
+ SQLModelFeatureMeta,
5
+ filter_feature_sqlmodel_metadata,
6
+ )
7
+
8
+ __all__ = [
9
+ "SQLModelFeatureMeta",
10
+ "BaseSQLModelFeature",
11
+ "SQLModelPluginConfig",
12
+ "filter_feature_sqlmodel_metadata",
13
+ ]
@@ -0,0 +1,29 @@
1
+ """Configuration for SQLModel integration."""
2
+
3
+ from pydantic import Field as PydanticField
4
+ from pydantic_settings import SettingsConfigDict
5
+
6
+ from metaxy.config import PluginConfig
7
+
8
+
9
+ class SQLModelPluginConfig(PluginConfig):
10
+ """Configuration for SQLModel integration.
11
+
12
+ This plugin enhances SQLModel-based features with automatic table name
13
+ inference and optional primary key injection.
14
+ """
15
+
16
+ model_config = SettingsConfigDict(
17
+ env_prefix="METAXY_EXT__SQLMODEL_",
18
+ extra="forbid",
19
+ )
20
+
21
+ inject_primary_key: bool = PydanticField(
22
+ default=False,
23
+ description="Automatically inject composite primary key constraints on SQLModel tables. The key is composed of ID columns, `metaxy_created_at`, and `metaxy_data_version`.",
24
+ )
25
+
26
+ inject_index: bool = PydanticField(
27
+ default=False,
28
+ description="Automatically inject composite index on SQLModel tables. The index covers ID columns, `metaxy_created_at`, and `metaxy_data_version`.",
29
+ )