metaxy 0.0.0__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.
Potentially problematic release.
This version of metaxy might be problematic. Click here for more details.
- metaxy/__init__.py +61 -0
- metaxy/_testing.py +542 -0
- metaxy/_utils.py +16 -0
- metaxy/_version.py +1 -0
- metaxy/cli/app.py +76 -0
- metaxy/cli/context.py +71 -0
- metaxy/cli/graph.py +576 -0
- metaxy/cli/graph_diff.py +290 -0
- metaxy/cli/list.py +42 -0
- metaxy/cli/metadata.py +271 -0
- metaxy/cli/migrations.py +862 -0
- metaxy/cli/push.py +55 -0
- metaxy/config.py +450 -0
- metaxy/data_versioning/__init__.py +24 -0
- metaxy/data_versioning/calculators/__init__.py +13 -0
- metaxy/data_versioning/calculators/base.py +97 -0
- metaxy/data_versioning/calculators/duckdb.py +186 -0
- metaxy/data_versioning/calculators/ibis.py +225 -0
- metaxy/data_versioning/calculators/polars.py +135 -0
- metaxy/data_versioning/diff/__init__.py +15 -0
- metaxy/data_versioning/diff/base.py +150 -0
- metaxy/data_versioning/diff/narwhals.py +108 -0
- metaxy/data_versioning/hash_algorithms.py +19 -0
- metaxy/data_versioning/joiners/__init__.py +9 -0
- metaxy/data_versioning/joiners/base.py +70 -0
- metaxy/data_versioning/joiners/narwhals.py +235 -0
- metaxy/entrypoints.py +309 -0
- metaxy/ext/__init__.py +1 -0
- metaxy/ext/alembic.py +326 -0
- metaxy/ext/sqlmodel.py +172 -0
- metaxy/ext/sqlmodel_system_tables.py +139 -0
- metaxy/graph/__init__.py +21 -0
- metaxy/graph/diff/__init__.py +21 -0
- metaxy/graph/diff/diff_models.py +399 -0
- metaxy/graph/diff/differ.py +740 -0
- metaxy/graph/diff/models.py +418 -0
- metaxy/graph/diff/rendering/__init__.py +18 -0
- metaxy/graph/diff/rendering/base.py +274 -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 +320 -0
- metaxy/graph/diff/rendering/rich.py +165 -0
- metaxy/graph/diff/rendering/theme.py +48 -0
- metaxy/graph/diff/traversal.py +247 -0
- metaxy/graph/utils.py +58 -0
- metaxy/metadata_store/__init__.py +31 -0
- metaxy/metadata_store/_protocols.py +38 -0
- metaxy/metadata_store/base.py +1676 -0
- metaxy/metadata_store/clickhouse.py +161 -0
- metaxy/metadata_store/duckdb.py +167 -0
- metaxy/metadata_store/exceptions.py +43 -0
- metaxy/metadata_store/ibis.py +451 -0
- metaxy/metadata_store/memory.py +228 -0
- metaxy/metadata_store/sqlite.py +187 -0
- metaxy/metadata_store/system_tables.py +257 -0
- metaxy/migrations/__init__.py +34 -0
- metaxy/migrations/detector.py +153 -0
- metaxy/migrations/executor.py +208 -0
- metaxy/migrations/loader.py +260 -0
- metaxy/migrations/models.py +718 -0
- metaxy/migrations/ops.py +390 -0
- metaxy/models/__init__.py +0 -0
- metaxy/models/bases.py +6 -0
- metaxy/models/constants.py +24 -0
- metaxy/models/feature.py +665 -0
- metaxy/models/feature_spec.py +105 -0
- metaxy/models/field.py +25 -0
- metaxy/models/plan.py +155 -0
- metaxy/models/types.py +157 -0
- metaxy/py.typed +0 -0
- metaxy-0.0.0.dist-info/METADATA +247 -0
- metaxy-0.0.0.dist-info/RECORD +75 -0
- metaxy-0.0.0.dist-info/WHEEL +4 -0
- metaxy-0.0.0.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
"""Graph diff models for migration system.
|
|
2
|
+
|
|
3
|
+
Provides GraphDiff with struct serialization for storage in migration tables.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from pydantic import Field
|
|
9
|
+
|
|
10
|
+
from metaxy.models.bases import FrozenBaseModel
|
|
11
|
+
from metaxy.models.types import FeatureKey, FieldKey
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FieldChange(FrozenBaseModel):
|
|
15
|
+
"""Represents a change in a field between two snapshots."""
|
|
16
|
+
|
|
17
|
+
field_key: FieldKey
|
|
18
|
+
old_version: str | None = None # None if field was added
|
|
19
|
+
new_version: str | None = None # None if field was removed
|
|
20
|
+
old_code_version: int | None = None
|
|
21
|
+
new_code_version: int | None = None
|
|
22
|
+
|
|
23
|
+
@property
|
|
24
|
+
def is_added(self) -> bool:
|
|
25
|
+
"""Check if field was added."""
|
|
26
|
+
return self.old_version is None
|
|
27
|
+
|
|
28
|
+
@property
|
|
29
|
+
def is_removed(self) -> bool:
|
|
30
|
+
"""Check if field was removed."""
|
|
31
|
+
return self.new_version is None
|
|
32
|
+
|
|
33
|
+
@property
|
|
34
|
+
def is_changed(self) -> bool:
|
|
35
|
+
"""Check if field version changed."""
|
|
36
|
+
return (
|
|
37
|
+
self.old_version is not None
|
|
38
|
+
and self.new_version is not None
|
|
39
|
+
and self.old_version != self.new_version
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class NodeChange(FrozenBaseModel):
|
|
44
|
+
"""Represents a change in a node/feature between two snapshots."""
|
|
45
|
+
|
|
46
|
+
feature_key: FeatureKey
|
|
47
|
+
old_version: str | None = None # None if node was added
|
|
48
|
+
new_version: str | None = None # None if node was removed
|
|
49
|
+
old_code_version: int | None = None
|
|
50
|
+
new_code_version: int | None = None
|
|
51
|
+
added_fields: list[FieldChange] = Field(default_factory=list)
|
|
52
|
+
removed_fields: list[FieldChange] = Field(default_factory=list)
|
|
53
|
+
changed_fields: list[FieldChange] = Field(default_factory=list)
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def is_added(self) -> bool:
|
|
57
|
+
"""Check if node was added."""
|
|
58
|
+
return self.old_version is None
|
|
59
|
+
|
|
60
|
+
@property
|
|
61
|
+
def is_removed(self) -> bool:
|
|
62
|
+
"""Check if node was removed."""
|
|
63
|
+
return self.new_version is None
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def is_changed(self) -> bool:
|
|
67
|
+
"""Check if node version changed."""
|
|
68
|
+
return (
|
|
69
|
+
self.old_version is not None
|
|
70
|
+
and self.new_version is not None
|
|
71
|
+
and self.old_version != self.new_version
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
@property
|
|
75
|
+
def field_changes(self) -> list[FieldChange]:
|
|
76
|
+
"""Get all field changes (added + removed + changed).
|
|
77
|
+
|
|
78
|
+
Backward compatibility property for old API.
|
|
79
|
+
"""
|
|
80
|
+
return self.added_fields + self.removed_fields + self.changed_fields
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def has_field_changes(self) -> bool:
|
|
84
|
+
"""Check if node has any field changes.
|
|
85
|
+
|
|
86
|
+
Backward compatibility property for old API.
|
|
87
|
+
"""
|
|
88
|
+
return bool(self.added_fields or self.removed_fields or self.changed_fields)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class AddedNode(FrozenBaseModel):
|
|
92
|
+
"""Represents a node that was added in the diff."""
|
|
93
|
+
|
|
94
|
+
feature_key: FeatureKey
|
|
95
|
+
version: str
|
|
96
|
+
code_version: int | None = None
|
|
97
|
+
fields: list[dict[str, Any]] = Field(
|
|
98
|
+
default_factory=list
|
|
99
|
+
) # {key, version, code_version}
|
|
100
|
+
dependencies: list[FeatureKey] = Field(default_factory=list)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class RemovedNode(FrozenBaseModel):
|
|
104
|
+
"""Represents a node that was removed in the diff."""
|
|
105
|
+
|
|
106
|
+
feature_key: FeatureKey
|
|
107
|
+
version: str
|
|
108
|
+
code_version: int | None = None
|
|
109
|
+
fields: list[dict[str, Any]] = Field(
|
|
110
|
+
default_factory=list
|
|
111
|
+
) # {key, version, code_version}
|
|
112
|
+
dependencies: list[FeatureKey] = Field(default_factory=list)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
class GraphDiff(FrozenBaseModel):
|
|
116
|
+
"""Result of comparing two graph snapshots.
|
|
117
|
+
|
|
118
|
+
Stores changes between two graph states for migration generation.
|
|
119
|
+
"""
|
|
120
|
+
|
|
121
|
+
from_snapshot_version: str
|
|
122
|
+
to_snapshot_version: str
|
|
123
|
+
added_nodes: list[AddedNode] = Field(default_factory=list)
|
|
124
|
+
removed_nodes: list[RemovedNode] = Field(default_factory=list)
|
|
125
|
+
changed_nodes: list[NodeChange] = Field(default_factory=list)
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def has_changes(self) -> bool:
|
|
129
|
+
"""Check if diff contains any changes."""
|
|
130
|
+
return bool(self.added_nodes or self.removed_nodes or self.changed_nodes)
|
|
131
|
+
|
|
132
|
+
def to_struct(self) -> dict[str, Any]:
|
|
133
|
+
"""Serialize to struct (native Python types for storage).
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
Dict with structure compatible with Polars struct type
|
|
137
|
+
"""
|
|
138
|
+
added_nodes_list = []
|
|
139
|
+
for node in self.added_nodes:
|
|
140
|
+
fields_list = []
|
|
141
|
+
for field in node.fields:
|
|
142
|
+
fields_list.append(
|
|
143
|
+
{
|
|
144
|
+
"key": field["key"]
|
|
145
|
+
if isinstance(field["key"], str)
|
|
146
|
+
else field["key"].to_string(),
|
|
147
|
+
"version": field.get("version", ""),
|
|
148
|
+
"code_version": field.get("code_version", 0) or 0,
|
|
149
|
+
}
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
added_nodes_list.append(
|
|
153
|
+
{
|
|
154
|
+
"key": node.feature_key.to_string(),
|
|
155
|
+
"version": node.version,
|
|
156
|
+
"code_version": node.code_version or 0,
|
|
157
|
+
"fields": fields_list,
|
|
158
|
+
"dependencies": [dep.to_string() for dep in node.dependencies],
|
|
159
|
+
}
|
|
160
|
+
)
|
|
161
|
+
|
|
162
|
+
removed_nodes_list = []
|
|
163
|
+
for node in self.removed_nodes:
|
|
164
|
+
fields_list = []
|
|
165
|
+
for field in node.fields:
|
|
166
|
+
fields_list.append(
|
|
167
|
+
{
|
|
168
|
+
"key": field["key"]
|
|
169
|
+
if isinstance(field["key"], str)
|
|
170
|
+
else field["key"].to_string(),
|
|
171
|
+
"version": field.get("version", ""),
|
|
172
|
+
"code_version": field.get("code_version", 0) or 0,
|
|
173
|
+
}
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
removed_nodes_list.append(
|
|
177
|
+
{
|
|
178
|
+
"key": node.feature_key.to_string(),
|
|
179
|
+
"version": node.version,
|
|
180
|
+
"code_version": node.code_version or 0,
|
|
181
|
+
"fields": fields_list,
|
|
182
|
+
"dependencies": [dep.to_string() for dep in node.dependencies],
|
|
183
|
+
}
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
changed_nodes_list = []
|
|
187
|
+
for node in self.changed_nodes:
|
|
188
|
+
added_fields_list = []
|
|
189
|
+
for field in node.added_fields:
|
|
190
|
+
added_fields_list.append(
|
|
191
|
+
{
|
|
192
|
+
"key": field.field_key.to_string(),
|
|
193
|
+
"version": field.new_version or "",
|
|
194
|
+
"code_version": field.new_code_version or 0,
|
|
195
|
+
}
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
removed_fields_list = []
|
|
199
|
+
for field in node.removed_fields:
|
|
200
|
+
removed_fields_list.append(
|
|
201
|
+
{
|
|
202
|
+
"key": field.field_key.to_string(),
|
|
203
|
+
"version": field.old_version or "",
|
|
204
|
+
"code_version": field.old_code_version or 0,
|
|
205
|
+
}
|
|
206
|
+
)
|
|
207
|
+
|
|
208
|
+
changed_fields_list = []
|
|
209
|
+
for field in node.changed_fields:
|
|
210
|
+
changed_fields_list.append(
|
|
211
|
+
{
|
|
212
|
+
"key": field.field_key.to_string(),
|
|
213
|
+
"old_version": field.old_version or "",
|
|
214
|
+
"new_version": field.new_version or "",
|
|
215
|
+
"old_code_version": field.old_code_version or 0,
|
|
216
|
+
"new_code_version": field.new_code_version or 0,
|
|
217
|
+
}
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
changed_nodes_list.append(
|
|
221
|
+
{
|
|
222
|
+
"key": node.feature_key.to_string(),
|
|
223
|
+
"old_version": node.old_version or "",
|
|
224
|
+
"new_version": node.new_version or "",
|
|
225
|
+
"old_code_version": node.old_code_version or 0,
|
|
226
|
+
"new_code_version": node.new_code_version or 0,
|
|
227
|
+
"added_fields": added_fields_list,
|
|
228
|
+
"removed_fields": removed_fields_list,
|
|
229
|
+
"changed_fields": changed_fields_list,
|
|
230
|
+
}
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
"added_nodes": added_nodes_list,
|
|
235
|
+
"removed_nodes": removed_nodes_list,
|
|
236
|
+
"changed_nodes": changed_nodes_list,
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
@classmethod
|
|
240
|
+
def from_struct(
|
|
241
|
+
cls,
|
|
242
|
+
struct_data: dict[str, Any],
|
|
243
|
+
from_snapshot_version: str,
|
|
244
|
+
to_snapshot_version: str,
|
|
245
|
+
) -> "GraphDiff":
|
|
246
|
+
"""Deserialize from struct.
|
|
247
|
+
|
|
248
|
+
Args:
|
|
249
|
+
struct_data: Dict with structure from to_struct()
|
|
250
|
+
from_snapshot_version: Source snapshot version
|
|
251
|
+
to_snapshot_version: Target snapshot version
|
|
252
|
+
|
|
253
|
+
Returns:
|
|
254
|
+
GraphDiff instance
|
|
255
|
+
"""
|
|
256
|
+
added_nodes = []
|
|
257
|
+
for node_data in struct_data.get("added_nodes", []):
|
|
258
|
+
fields = []
|
|
259
|
+
for field_data in node_data.get("fields", []):
|
|
260
|
+
fields.append(
|
|
261
|
+
{
|
|
262
|
+
"key": field_data["key"],
|
|
263
|
+
"version": field_data["version"]
|
|
264
|
+
if field_data["version"]
|
|
265
|
+
else None,
|
|
266
|
+
"code_version": field_data["code_version"]
|
|
267
|
+
if field_data["code_version"] != 0
|
|
268
|
+
else None,
|
|
269
|
+
}
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
added_nodes.append(
|
|
273
|
+
AddedNode(
|
|
274
|
+
feature_key=FeatureKey(node_data["key"].split("/")),
|
|
275
|
+
version=node_data["version"],
|
|
276
|
+
code_version=node_data["code_version"]
|
|
277
|
+
if node_data["code_version"] != 0
|
|
278
|
+
else None,
|
|
279
|
+
fields=fields,
|
|
280
|
+
dependencies=[
|
|
281
|
+
FeatureKey(dep.split("/"))
|
|
282
|
+
for dep in node_data.get("dependencies", [])
|
|
283
|
+
],
|
|
284
|
+
)
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
removed_nodes = []
|
|
288
|
+
for node_data in struct_data.get("removed_nodes", []):
|
|
289
|
+
fields = []
|
|
290
|
+
for field_data in node_data.get("fields", []):
|
|
291
|
+
fields.append(
|
|
292
|
+
{
|
|
293
|
+
"key": field_data["key"],
|
|
294
|
+
"version": field_data["version"]
|
|
295
|
+
if field_data["version"]
|
|
296
|
+
else None,
|
|
297
|
+
"code_version": field_data["code_version"]
|
|
298
|
+
if field_data["code_version"] != 0
|
|
299
|
+
else None,
|
|
300
|
+
}
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
removed_nodes.append(
|
|
304
|
+
RemovedNode(
|
|
305
|
+
feature_key=FeatureKey(node_data["key"].split("/")),
|
|
306
|
+
version=node_data["version"],
|
|
307
|
+
code_version=node_data["code_version"]
|
|
308
|
+
if node_data["code_version"] != 0
|
|
309
|
+
else None,
|
|
310
|
+
fields=fields,
|
|
311
|
+
dependencies=[
|
|
312
|
+
FeatureKey(dep.split("/"))
|
|
313
|
+
for dep in node_data.get("dependencies", [])
|
|
314
|
+
],
|
|
315
|
+
)
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
changed_nodes = []
|
|
319
|
+
for node_data in struct_data.get("changed_nodes", []):
|
|
320
|
+
added_fields = []
|
|
321
|
+
for field_data in node_data.get("added_fields", []):
|
|
322
|
+
added_fields.append(
|
|
323
|
+
FieldChange(
|
|
324
|
+
field_key=FieldKey(field_data["key"].split("/")),
|
|
325
|
+
old_version=None,
|
|
326
|
+
new_version=field_data["version"]
|
|
327
|
+
if field_data["version"]
|
|
328
|
+
else None,
|
|
329
|
+
old_code_version=None,
|
|
330
|
+
new_code_version=field_data["code_version"]
|
|
331
|
+
if field_data["code_version"] != 0
|
|
332
|
+
else None,
|
|
333
|
+
)
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
removed_fields = []
|
|
337
|
+
for field_data in node_data.get("removed_fields", []):
|
|
338
|
+
removed_fields.append(
|
|
339
|
+
FieldChange(
|
|
340
|
+
field_key=FieldKey(field_data["key"].split("/")),
|
|
341
|
+
old_version=field_data["version"]
|
|
342
|
+
if field_data["version"]
|
|
343
|
+
else None,
|
|
344
|
+
new_version=None,
|
|
345
|
+
old_code_version=field_data["code_version"]
|
|
346
|
+
if field_data["code_version"] != 0
|
|
347
|
+
else None,
|
|
348
|
+
new_code_version=None,
|
|
349
|
+
)
|
|
350
|
+
)
|
|
351
|
+
|
|
352
|
+
changed_fields = []
|
|
353
|
+
for field_data in node_data.get("changed_fields", []):
|
|
354
|
+
changed_fields.append(
|
|
355
|
+
FieldChange(
|
|
356
|
+
field_key=FieldKey(field_data["key"].split("/")),
|
|
357
|
+
old_version=field_data["old_version"]
|
|
358
|
+
if field_data["old_version"]
|
|
359
|
+
else None,
|
|
360
|
+
new_version=field_data["new_version"]
|
|
361
|
+
if field_data["new_version"]
|
|
362
|
+
else None,
|
|
363
|
+
old_code_version=field_data["old_code_version"]
|
|
364
|
+
if field_data["old_code_version"] != 0
|
|
365
|
+
else None,
|
|
366
|
+
new_code_version=field_data["new_code_version"]
|
|
367
|
+
if field_data["new_code_version"] != 0
|
|
368
|
+
else None,
|
|
369
|
+
)
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
changed_nodes.append(
|
|
373
|
+
NodeChange(
|
|
374
|
+
feature_key=FeatureKey(node_data["key"].split("/")),
|
|
375
|
+
old_version=node_data["old_version"]
|
|
376
|
+
if node_data["old_version"]
|
|
377
|
+
else None,
|
|
378
|
+
new_version=node_data["new_version"]
|
|
379
|
+
if node_data["new_version"]
|
|
380
|
+
else None,
|
|
381
|
+
old_code_version=node_data["old_code_version"]
|
|
382
|
+
if node_data["old_code_version"] != 0
|
|
383
|
+
else None,
|
|
384
|
+
new_code_version=node_data["new_code_version"]
|
|
385
|
+
if node_data["new_code_version"] != 0
|
|
386
|
+
else None,
|
|
387
|
+
added_fields=added_fields,
|
|
388
|
+
removed_fields=removed_fields,
|
|
389
|
+
changed_fields=changed_fields,
|
|
390
|
+
)
|
|
391
|
+
)
|
|
392
|
+
|
|
393
|
+
return cls(
|
|
394
|
+
from_snapshot_version=from_snapshot_version,
|
|
395
|
+
to_snapshot_version=to_snapshot_version,
|
|
396
|
+
added_nodes=added_nodes,
|
|
397
|
+
removed_nodes=removed_nodes,
|
|
398
|
+
changed_nodes=changed_nodes,
|
|
399
|
+
)
|