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.

Files changed (75) hide show
  1. metaxy/__init__.py +61 -0
  2. metaxy/_testing.py +542 -0
  3. metaxy/_utils.py +16 -0
  4. metaxy/_version.py +1 -0
  5. metaxy/cli/app.py +76 -0
  6. metaxy/cli/context.py +71 -0
  7. metaxy/cli/graph.py +576 -0
  8. metaxy/cli/graph_diff.py +290 -0
  9. metaxy/cli/list.py +42 -0
  10. metaxy/cli/metadata.py +271 -0
  11. metaxy/cli/migrations.py +862 -0
  12. metaxy/cli/push.py +55 -0
  13. metaxy/config.py +450 -0
  14. metaxy/data_versioning/__init__.py +24 -0
  15. metaxy/data_versioning/calculators/__init__.py +13 -0
  16. metaxy/data_versioning/calculators/base.py +97 -0
  17. metaxy/data_versioning/calculators/duckdb.py +186 -0
  18. metaxy/data_versioning/calculators/ibis.py +225 -0
  19. metaxy/data_versioning/calculators/polars.py +135 -0
  20. metaxy/data_versioning/diff/__init__.py +15 -0
  21. metaxy/data_versioning/diff/base.py +150 -0
  22. metaxy/data_versioning/diff/narwhals.py +108 -0
  23. metaxy/data_versioning/hash_algorithms.py +19 -0
  24. metaxy/data_versioning/joiners/__init__.py +9 -0
  25. metaxy/data_versioning/joiners/base.py +70 -0
  26. metaxy/data_versioning/joiners/narwhals.py +235 -0
  27. metaxy/entrypoints.py +309 -0
  28. metaxy/ext/__init__.py +1 -0
  29. metaxy/ext/alembic.py +326 -0
  30. metaxy/ext/sqlmodel.py +172 -0
  31. metaxy/ext/sqlmodel_system_tables.py +139 -0
  32. metaxy/graph/__init__.py +21 -0
  33. metaxy/graph/diff/__init__.py +21 -0
  34. metaxy/graph/diff/diff_models.py +399 -0
  35. metaxy/graph/diff/differ.py +740 -0
  36. metaxy/graph/diff/models.py +418 -0
  37. metaxy/graph/diff/rendering/__init__.py +18 -0
  38. metaxy/graph/diff/rendering/base.py +274 -0
  39. metaxy/graph/diff/rendering/cards.py +188 -0
  40. metaxy/graph/diff/rendering/formatter.py +805 -0
  41. metaxy/graph/diff/rendering/graphviz.py +246 -0
  42. metaxy/graph/diff/rendering/mermaid.py +320 -0
  43. metaxy/graph/diff/rendering/rich.py +165 -0
  44. metaxy/graph/diff/rendering/theme.py +48 -0
  45. metaxy/graph/diff/traversal.py +247 -0
  46. metaxy/graph/utils.py +58 -0
  47. metaxy/metadata_store/__init__.py +31 -0
  48. metaxy/metadata_store/_protocols.py +38 -0
  49. metaxy/metadata_store/base.py +1676 -0
  50. metaxy/metadata_store/clickhouse.py +161 -0
  51. metaxy/metadata_store/duckdb.py +167 -0
  52. metaxy/metadata_store/exceptions.py +43 -0
  53. metaxy/metadata_store/ibis.py +451 -0
  54. metaxy/metadata_store/memory.py +228 -0
  55. metaxy/metadata_store/sqlite.py +187 -0
  56. metaxy/metadata_store/system_tables.py +257 -0
  57. metaxy/migrations/__init__.py +34 -0
  58. metaxy/migrations/detector.py +153 -0
  59. metaxy/migrations/executor.py +208 -0
  60. metaxy/migrations/loader.py +260 -0
  61. metaxy/migrations/models.py +718 -0
  62. metaxy/migrations/ops.py +390 -0
  63. metaxy/models/__init__.py +0 -0
  64. metaxy/models/bases.py +6 -0
  65. metaxy/models/constants.py +24 -0
  66. metaxy/models/feature.py +665 -0
  67. metaxy/models/feature_spec.py +105 -0
  68. metaxy/models/field.py +25 -0
  69. metaxy/models/plan.py +155 -0
  70. metaxy/models/types.py +157 -0
  71. metaxy/py.typed +0 -0
  72. metaxy-0.0.0.dist-info/METADATA +247 -0
  73. metaxy-0.0.0.dist-info/RECORD +75 -0
  74. metaxy-0.0.0.dist-info/WHEEL +4 -0
  75. 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
+ )