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
metaxy/cli/graph_diff.py
ADDED
|
@@ -0,0 +1,290 @@
|
|
|
1
|
+
"""Graph diff commands for Metaxy CLI."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated, Literal
|
|
4
|
+
|
|
5
|
+
import cyclopts
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from metaxy.graph import RenderConfig
|
|
9
|
+
|
|
10
|
+
# Rich console for formatted output
|
|
11
|
+
console = Console()
|
|
12
|
+
|
|
13
|
+
# Graph-diff subcommand app
|
|
14
|
+
app = cyclopts.App(
|
|
15
|
+
name="graph-diff", # pyrefly: ignore[unexpected-keyword]
|
|
16
|
+
help="Compare and visualize graph snapshots", # pyrefly: ignore[unexpected-keyword]
|
|
17
|
+
console=console, # pyrefly: ignore[unexpected-keyword]
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@app.command()
|
|
22
|
+
def render(
|
|
23
|
+
from_snapshot: Annotated[
|
|
24
|
+
str,
|
|
25
|
+
cyclopts.Parameter(
|
|
26
|
+
help='First snapshot to compare (can be "latest", "current", or snapshot hash)',
|
|
27
|
+
),
|
|
28
|
+
],
|
|
29
|
+
to_snapshot: Annotated[
|
|
30
|
+
str,
|
|
31
|
+
cyclopts.Parameter(
|
|
32
|
+
help='Second snapshot to compare (can be "latest", "current", or snapshot hash)',
|
|
33
|
+
),
|
|
34
|
+
] = "current",
|
|
35
|
+
store: Annotated[
|
|
36
|
+
str | None,
|
|
37
|
+
cyclopts.Parameter(
|
|
38
|
+
name=["--store"],
|
|
39
|
+
help="Metadata store to use (defaults to configured default store)",
|
|
40
|
+
),
|
|
41
|
+
] = None,
|
|
42
|
+
format: Annotated[
|
|
43
|
+
Literal["terminal", "cards", "mermaid", "graphviz", "json", "yaml"],
|
|
44
|
+
cyclopts.Parameter(
|
|
45
|
+
name=["--format", "-f"],
|
|
46
|
+
help="Output format: terminal, cards, mermaid, graphviz, json, or yaml",
|
|
47
|
+
),
|
|
48
|
+
] = "terminal",
|
|
49
|
+
output: Annotated[
|
|
50
|
+
str | None,
|
|
51
|
+
cyclopts.Parameter(
|
|
52
|
+
name=["--output", "-o"],
|
|
53
|
+
help="Output file path (default: stdout)",
|
|
54
|
+
),
|
|
55
|
+
] = None,
|
|
56
|
+
config: Annotated[
|
|
57
|
+
RenderConfig | None,
|
|
58
|
+
cyclopts.Parameter(name="*", help="Render configuration"),
|
|
59
|
+
] = None,
|
|
60
|
+
# Preset modes
|
|
61
|
+
minimal: Annotated[
|
|
62
|
+
bool,
|
|
63
|
+
cyclopts.Parameter(
|
|
64
|
+
name=["--minimal"],
|
|
65
|
+
help="Minimal output: only feature keys and dependencies",
|
|
66
|
+
),
|
|
67
|
+
] = False,
|
|
68
|
+
verbose: Annotated[
|
|
69
|
+
bool,
|
|
70
|
+
cyclopts.Parameter(
|
|
71
|
+
name=["--verbose"],
|
|
72
|
+
help="Verbose output: show all available information",
|
|
73
|
+
),
|
|
74
|
+
] = False,
|
|
75
|
+
):
|
|
76
|
+
"""Render merged graph visualization comparing two snapshots.
|
|
77
|
+
|
|
78
|
+
Shows all features color-coded by status (added/removed/changed/unchanged).
|
|
79
|
+
Uses the unified rendering system - same renderers as 'metaxy graph render'.
|
|
80
|
+
|
|
81
|
+
Special snapshot literals:
|
|
82
|
+
- "latest": Most recent snapshot in the store
|
|
83
|
+
- "current": Current graph state from code
|
|
84
|
+
|
|
85
|
+
Output formats:
|
|
86
|
+
- terminal: Hierarchical tree view (default)
|
|
87
|
+
- cards: Panel/card-based view
|
|
88
|
+
- mermaid: Mermaid flowchart diagram
|
|
89
|
+
- graphviz: Graphviz DOT format
|
|
90
|
+
|
|
91
|
+
Examples:
|
|
92
|
+
# Show merged graph with default terminal renderer
|
|
93
|
+
$ metaxy graph-diff render latest current
|
|
94
|
+
|
|
95
|
+
# Cards view
|
|
96
|
+
$ metaxy graph-diff render latest current --format cards
|
|
97
|
+
|
|
98
|
+
# Focus on specific feature with 2 levels up and 1 level down
|
|
99
|
+
$ metaxy graph-diff render latest current --feature user/profile --up 2 --down 1
|
|
100
|
+
|
|
101
|
+
# Show only changed fields (hide unchanged)
|
|
102
|
+
$ metaxy graph-diff render latest current --show-changed-fields-only
|
|
103
|
+
|
|
104
|
+
# Save Mermaid diagram to file
|
|
105
|
+
$ metaxy graph-diff render latest current --format mermaid --output diff.mmd
|
|
106
|
+
|
|
107
|
+
# Graphviz DOT format
|
|
108
|
+
$ metaxy graph-diff render latest current --format graphviz --output diff.dot
|
|
109
|
+
|
|
110
|
+
# Minimal view
|
|
111
|
+
$ metaxy graph-diff render latest current --minimal
|
|
112
|
+
|
|
113
|
+
# Everything
|
|
114
|
+
$ metaxy graph-diff render latest current --verbose
|
|
115
|
+
"""
|
|
116
|
+
from metaxy.cli.context import get_store
|
|
117
|
+
from metaxy.entrypoints import load_features
|
|
118
|
+
from metaxy.graph import (
|
|
119
|
+
CardsRenderer,
|
|
120
|
+
GraphData,
|
|
121
|
+
GraphvizRenderer,
|
|
122
|
+
)
|
|
123
|
+
from metaxy.graph.diff.differ import GraphDiffer, SnapshotResolver
|
|
124
|
+
from metaxy.models.feature import FeatureGraph
|
|
125
|
+
|
|
126
|
+
# Validate format
|
|
127
|
+
valid_formats = ["terminal", "cards", "mermaid", "graphviz", "json", "yaml"]
|
|
128
|
+
if format not in valid_formats:
|
|
129
|
+
console.print(
|
|
130
|
+
f"[red]Error:[/red] Invalid format '{format}'. Must be one of: {', '.join(valid_formats)}"
|
|
131
|
+
)
|
|
132
|
+
raise SystemExit(1)
|
|
133
|
+
|
|
134
|
+
# Resolve configuration from presets
|
|
135
|
+
if minimal and verbose:
|
|
136
|
+
console.print("[red]Error:[/red] Cannot specify both --minimal and --verbose")
|
|
137
|
+
raise SystemExit(1)
|
|
138
|
+
|
|
139
|
+
# If config is None, create a default instance
|
|
140
|
+
if config is None:
|
|
141
|
+
config = RenderConfig()
|
|
142
|
+
|
|
143
|
+
# Apply presets if specified (overrides display settings but preserves filtering)
|
|
144
|
+
if minimal:
|
|
145
|
+
preset = RenderConfig.minimal()
|
|
146
|
+
# Preserve filtering parameters from original config
|
|
147
|
+
preset.feature = config.feature
|
|
148
|
+
preset.up = config.up
|
|
149
|
+
preset.down = config.down
|
|
150
|
+
config = preset
|
|
151
|
+
elif verbose:
|
|
152
|
+
preset = RenderConfig.verbose()
|
|
153
|
+
# Preserve filtering parameters from original config
|
|
154
|
+
preset.feature = config.feature
|
|
155
|
+
preset.up = config.up
|
|
156
|
+
preset.down = config.down
|
|
157
|
+
config = preset
|
|
158
|
+
|
|
159
|
+
# Validate filtering options
|
|
160
|
+
if (config.up is not None or config.down is not None) and config.feature is None:
|
|
161
|
+
console.print(
|
|
162
|
+
"[red]Error:[/red] --up and --down require --feature to be specified"
|
|
163
|
+
)
|
|
164
|
+
raise SystemExit(1)
|
|
165
|
+
|
|
166
|
+
# Load features from entrypoints (needed for "current" literal)
|
|
167
|
+
load_features()
|
|
168
|
+
graph = FeatureGraph.get_active()
|
|
169
|
+
|
|
170
|
+
metadata_store = get_store(store)
|
|
171
|
+
|
|
172
|
+
with metadata_store:
|
|
173
|
+
# Resolve snapshot versions
|
|
174
|
+
resolver = SnapshotResolver()
|
|
175
|
+
try:
|
|
176
|
+
from_snapshot_version = resolver.resolve_snapshot(
|
|
177
|
+
from_snapshot, metadata_store, graph
|
|
178
|
+
)
|
|
179
|
+
to_snapshot_version = resolver.resolve_snapshot(
|
|
180
|
+
to_snapshot, metadata_store, graph
|
|
181
|
+
)
|
|
182
|
+
except ValueError as e:
|
|
183
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
184
|
+
raise SystemExit(1)
|
|
185
|
+
|
|
186
|
+
# Load snapshot data
|
|
187
|
+
differ = GraphDiffer()
|
|
188
|
+
try:
|
|
189
|
+
from_snapshot_data = differ.load_snapshot_data(
|
|
190
|
+
metadata_store, from_snapshot_version
|
|
191
|
+
)
|
|
192
|
+
to_snapshot_data = differ.load_snapshot_data(
|
|
193
|
+
metadata_store, to_snapshot_version
|
|
194
|
+
)
|
|
195
|
+
except ValueError as e:
|
|
196
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
197
|
+
raise SystemExit(1)
|
|
198
|
+
|
|
199
|
+
# Compute diff
|
|
200
|
+
graph_diff = differ.diff(from_snapshot_data, to_snapshot_data)
|
|
201
|
+
|
|
202
|
+
# Create merged graph data
|
|
203
|
+
merged_data = differ.create_merged_graph_data(
|
|
204
|
+
from_snapshot_data, to_snapshot_data, graph_diff
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
# Apply graph slicing if requested
|
|
208
|
+
if config.feature is not None:
|
|
209
|
+
try:
|
|
210
|
+
merged_data = differ.filter_merged_graph(
|
|
211
|
+
merged_data,
|
|
212
|
+
focus_feature=config.feature,
|
|
213
|
+
up=config.up,
|
|
214
|
+
down=config.down,
|
|
215
|
+
)
|
|
216
|
+
except ValueError as e:
|
|
217
|
+
console.print(f"[red]Error:[/red] {e}")
|
|
218
|
+
raise SystemExit(1)
|
|
219
|
+
|
|
220
|
+
# Render the diff
|
|
221
|
+
# Use DiffFormatter for terminal/json/yaml/mermaid (has proper diff visualization)
|
|
222
|
+
# Use unified renderers for cards/graphviz (DiffFormatter doesn't support these)
|
|
223
|
+
if format in ("terminal", "mermaid", "json", "yaml"):
|
|
224
|
+
from metaxy.graph.diff.rendering.formatter import DiffFormatter
|
|
225
|
+
|
|
226
|
+
formatter = DiffFormatter(console)
|
|
227
|
+
|
|
228
|
+
# Determine show_all_fields based on config
|
|
229
|
+
# TODO: add show_changed_fields_only to config
|
|
230
|
+
show_all_fields = True # Default: show all fields
|
|
231
|
+
|
|
232
|
+
try:
|
|
233
|
+
rendered = formatter.format(
|
|
234
|
+
merged_data=merged_data,
|
|
235
|
+
format=format,
|
|
236
|
+
verbose=verbose,
|
|
237
|
+
diff_only=False, # Always use merged view for graph-diff render
|
|
238
|
+
show_all_fields=show_all_fields,
|
|
239
|
+
)
|
|
240
|
+
except Exception as e:
|
|
241
|
+
console.print(f"[red]Error:[/red] Rendering failed: {e}")
|
|
242
|
+
import traceback
|
|
243
|
+
|
|
244
|
+
traceback.print_exc()
|
|
245
|
+
raise SystemExit(1)
|
|
246
|
+
else:
|
|
247
|
+
# Use unified renderers for cards/graphviz formats
|
|
248
|
+
from metaxy.graph.diff.rendering.theme import Theme
|
|
249
|
+
|
|
250
|
+
theme = Theme.default()
|
|
251
|
+
graph_data = GraphData.from_merged_diff(merged_data)
|
|
252
|
+
|
|
253
|
+
if format == "cards":
|
|
254
|
+
renderer = CardsRenderer(
|
|
255
|
+
graph_data=graph_data, config=config, theme=theme
|
|
256
|
+
)
|
|
257
|
+
elif format == "graphviz":
|
|
258
|
+
renderer = GraphvizRenderer(
|
|
259
|
+
graph_data=graph_data, config=config, theme=theme
|
|
260
|
+
)
|
|
261
|
+
else:
|
|
262
|
+
console.print(f"[red]Error:[/red] Unknown format: {format}")
|
|
263
|
+
raise SystemExit(1)
|
|
264
|
+
|
|
265
|
+
try:
|
|
266
|
+
rendered = renderer.render()
|
|
267
|
+
except Exception as e:
|
|
268
|
+
console.print(f"[red]Error:[/red] Rendering failed: {e}")
|
|
269
|
+
import traceback
|
|
270
|
+
|
|
271
|
+
traceback.print_exc()
|
|
272
|
+
raise SystemExit(1)
|
|
273
|
+
|
|
274
|
+
# Output to file or stdout
|
|
275
|
+
if output:
|
|
276
|
+
try:
|
|
277
|
+
with open(output, "w") as f:
|
|
278
|
+
f.write(rendered)
|
|
279
|
+
console.print(f"[green]Success:[/green] Diff rendered to: {output}")
|
|
280
|
+
except Exception as e:
|
|
281
|
+
console.print(f"[red]Error:[/red] Failed to write to file: {e}")
|
|
282
|
+
raise SystemExit(1)
|
|
283
|
+
else:
|
|
284
|
+
# Print to stdout
|
|
285
|
+
if format in ("terminal", "cards"):
|
|
286
|
+
# Use plain print for terminal formats (they have ANSI codes)
|
|
287
|
+
print(rendered)
|
|
288
|
+
else:
|
|
289
|
+
# Use Rich console for non-terminal formats
|
|
290
|
+
console.print(rendered)
|
metaxy/cli/list.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import cyclopts
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
|
|
4
|
+
# Rich console for formatted output
|
|
5
|
+
console = Console()
|
|
6
|
+
|
|
7
|
+
# Migrations subcommand app
|
|
8
|
+
app = cyclopts.App(
|
|
9
|
+
name="list", # pyrefly: ignore[unexpected-keyword]
|
|
10
|
+
help="List Metaxy entities", # pyrefly: ignore[unexpected-keyword]
|
|
11
|
+
console=console, # pyrefly: ignore[unexpected-keyword]
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@app.command()
|
|
16
|
+
def features():
|
|
17
|
+
"""List Metaxy features"""
|
|
18
|
+
from metaxy.cli.context import set_config
|
|
19
|
+
from metaxy.config import MetaxyConfig
|
|
20
|
+
from metaxy.entrypoints import load_features
|
|
21
|
+
from metaxy.models.plan import FQFieldKey
|
|
22
|
+
|
|
23
|
+
metaxy_config = MetaxyConfig.load(search_parents=True)
|
|
24
|
+
|
|
25
|
+
set_config(metaxy_config)
|
|
26
|
+
|
|
27
|
+
graph = load_features()
|
|
28
|
+
|
|
29
|
+
for feature_key, feature_spec in graph.feature_specs_by_key.items():
|
|
30
|
+
console.print("---")
|
|
31
|
+
console.print(
|
|
32
|
+
f"{feature_key} (version {graph.get_feature_version(feature_key)})"
|
|
33
|
+
)
|
|
34
|
+
if feature_spec.deps:
|
|
35
|
+
console.print(" Feature Dependencies:")
|
|
36
|
+
for dep in feature_spec.deps:
|
|
37
|
+
console.print(f" {dep}")
|
|
38
|
+
console.print(" Fields:")
|
|
39
|
+
for field_key, field_spec in feature_spec.fields_by_key.items():
|
|
40
|
+
console.print(
|
|
41
|
+
f" {field_spec.key.to_string()} (code_version {field_spec.code_version}, version {graph.get_field_version(FQFieldKey(feature=feature_key, field=field_key))})"
|
|
42
|
+
)
|
metaxy/cli/metadata.py
ADDED
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"""Metadata management commands for Metaxy CLI."""
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Annotated
|
|
4
|
+
|
|
5
|
+
import cyclopts
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
8
|
+
from metaxy.models.types import FeatureKey
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from metaxy.models.feature import Feature
|
|
12
|
+
|
|
13
|
+
# Rich console for formatted output
|
|
14
|
+
console = Console()
|
|
15
|
+
|
|
16
|
+
# Metadata subcommand app
|
|
17
|
+
app = cyclopts.App(
|
|
18
|
+
name="metadata", # pyrefly: ignore[unexpected-keyword]
|
|
19
|
+
help="Manage Metaxy metadata", # pyrefly: ignore[unexpected-keyword]
|
|
20
|
+
console=console, # pyrefly: ignore[unexpected-keyword]
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@app.command()
|
|
25
|
+
def copy(
|
|
26
|
+
from_store: Annotated[
|
|
27
|
+
str,
|
|
28
|
+
cyclopts.Parameter(
|
|
29
|
+
name=["--from", "FROM"],
|
|
30
|
+
help="Source store name (must be configured in metaxy.toml)",
|
|
31
|
+
),
|
|
32
|
+
],
|
|
33
|
+
to_store: Annotated[
|
|
34
|
+
str,
|
|
35
|
+
cyclopts.Parameter(
|
|
36
|
+
name=["--to", "TO"],
|
|
37
|
+
help="Destination store name (must be configured in metaxy.toml)",
|
|
38
|
+
),
|
|
39
|
+
],
|
|
40
|
+
features: Annotated[
|
|
41
|
+
list[str] | None,
|
|
42
|
+
cyclopts.Parameter(
|
|
43
|
+
name=["--feature"],
|
|
44
|
+
help="Feature key to copy (e.g., 'my_feature' or 'group/my_feature'). Can be repeated multiple times. If not specified, uses --all-features.",
|
|
45
|
+
),
|
|
46
|
+
] = None,
|
|
47
|
+
all_features: Annotated[
|
|
48
|
+
bool,
|
|
49
|
+
cyclopts.Parameter(
|
|
50
|
+
name=["--all-features"],
|
|
51
|
+
help="Copy all features from source store",
|
|
52
|
+
),
|
|
53
|
+
] = False,
|
|
54
|
+
from_snapshot: Annotated[
|
|
55
|
+
str | None,
|
|
56
|
+
cyclopts.Parameter(
|
|
57
|
+
name=["--snapshot"],
|
|
58
|
+
help="Snapshot version to copy (defaults to latest in source store). The snapshot_version is preserved in the destination.",
|
|
59
|
+
),
|
|
60
|
+
] = None,
|
|
61
|
+
incremental: Annotated[
|
|
62
|
+
bool,
|
|
63
|
+
cyclopts.Parameter(
|
|
64
|
+
name=["--incremental"],
|
|
65
|
+
help="Use incremental copy (compare data_version to skip existing rows). Disable for better performance if destination is empty or uses deduplication.",
|
|
66
|
+
),
|
|
67
|
+
] = True,
|
|
68
|
+
):
|
|
69
|
+
"""Copy metadata between stores.
|
|
70
|
+
|
|
71
|
+
Copies metadata for specified features from one store to another,
|
|
72
|
+
optionally using a historical version. Useful for:
|
|
73
|
+
- Migrating data between environments
|
|
74
|
+
- Backfilling metadata
|
|
75
|
+
- Copying specific feature versions
|
|
76
|
+
|
|
77
|
+
Incremental Mode (default):
|
|
78
|
+
By default, performs an anti-join on sample_uid to skip rows that already exist
|
|
79
|
+
in the destination for the same snapshot_version. This prevents duplicate writes.
|
|
80
|
+
|
|
81
|
+
Disabling incremental (--no-incremental) may improve performance when:
|
|
82
|
+
- The destination store is empty or has no overlap with source
|
|
83
|
+
- The destination store has eventual deduplication
|
|
84
|
+
|
|
85
|
+
Examples:
|
|
86
|
+
# Copy all features from latest snapshot in dev to staging
|
|
87
|
+
$ metaxy metadata copy --from dev --to staging --all-features
|
|
88
|
+
|
|
89
|
+
# Copy specific features (repeatable flag)
|
|
90
|
+
$ metaxy metadata copy --from dev --to staging --feature user_features --feature customer_features
|
|
91
|
+
|
|
92
|
+
# Copy specific snapshot
|
|
93
|
+
$ metaxy metadata copy --from prod --to staging --all-features --snapshot abc123
|
|
94
|
+
|
|
95
|
+
# Non-incremental copy (faster, but may create duplicates)
|
|
96
|
+
$ metaxy metadata copy --from dev --to staging --all-features --no-incremental
|
|
97
|
+
"""
|
|
98
|
+
import logging
|
|
99
|
+
|
|
100
|
+
from metaxy.cli.context import get_config
|
|
101
|
+
|
|
102
|
+
# Enable logging to show progress
|
|
103
|
+
logging.basicConfig(level=logging.INFO, format="%(message)s")
|
|
104
|
+
|
|
105
|
+
config = get_config()
|
|
106
|
+
|
|
107
|
+
# Validate arguments
|
|
108
|
+
if not all_features and not features:
|
|
109
|
+
console.print(
|
|
110
|
+
"[red]Error:[/red] Must specify either --all-features or --feature"
|
|
111
|
+
)
|
|
112
|
+
raise SystemExit(1)
|
|
113
|
+
|
|
114
|
+
if all_features and features:
|
|
115
|
+
console.print(
|
|
116
|
+
"[red]Error:[/red] Cannot specify both --all-features and --feature"
|
|
117
|
+
)
|
|
118
|
+
raise SystemExit(1)
|
|
119
|
+
|
|
120
|
+
# Parse feature keys
|
|
121
|
+
feature_keys: list[FeatureKey | type[Feature]] | None = None
|
|
122
|
+
if features:
|
|
123
|
+
feature_keys = []
|
|
124
|
+
for feature_str in features:
|
|
125
|
+
# Parse feature key (supports both "feature" and "part1/part2/..." formats)
|
|
126
|
+
if "/" in feature_str:
|
|
127
|
+
parts = feature_str.split("/")
|
|
128
|
+
feature_keys.append(FeatureKey(parts))
|
|
129
|
+
else:
|
|
130
|
+
# Single-part key
|
|
131
|
+
feature_keys.append(FeatureKey([feature_str]))
|
|
132
|
+
|
|
133
|
+
# Get stores
|
|
134
|
+
console.print(f"[cyan]Source store:[/cyan] {from_store}")
|
|
135
|
+
console.print(f"[cyan]Destination store:[/cyan] {to_store}")
|
|
136
|
+
|
|
137
|
+
source_store = config.get_store(from_store)
|
|
138
|
+
dest_store = config.get_store(to_store)
|
|
139
|
+
|
|
140
|
+
# Open both stores and copy
|
|
141
|
+
with source_store, dest_store:
|
|
142
|
+
console.print("\n[bold]Starting copy operation...[/bold]\n")
|
|
143
|
+
|
|
144
|
+
try:
|
|
145
|
+
stats = dest_store.copy_metadata(
|
|
146
|
+
from_store=source_store,
|
|
147
|
+
features=feature_keys,
|
|
148
|
+
from_snapshot=from_snapshot,
|
|
149
|
+
incremental=incremental,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
console.print(
|
|
153
|
+
f"\n[green]✓[/green] Copy complete: {stats['features_copied']} features, {stats['rows_copied']} rows"
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
except Exception as e:
|
|
157
|
+
console.print(f"\n[red]✗[/red] Copy failed:\n{e}")
|
|
158
|
+
raise SystemExit(1)
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
@app.command()
|
|
162
|
+
def drop(
|
|
163
|
+
store: Annotated[
|
|
164
|
+
str | None,
|
|
165
|
+
cyclopts.Parameter(
|
|
166
|
+
name=["--store"],
|
|
167
|
+
help="Store name to drop metadata from (defaults to configured default store)",
|
|
168
|
+
),
|
|
169
|
+
] = None,
|
|
170
|
+
features: Annotated[
|
|
171
|
+
list[str] | None,
|
|
172
|
+
cyclopts.Parameter(
|
|
173
|
+
name=["--feature"],
|
|
174
|
+
help="Feature key to drop (e.g., 'my_feature' or 'group/my_feature'). Can be repeated multiple times. If not specified, uses --all-features.",
|
|
175
|
+
),
|
|
176
|
+
] = None,
|
|
177
|
+
all_features: Annotated[
|
|
178
|
+
bool,
|
|
179
|
+
cyclopts.Parameter(
|
|
180
|
+
name=["--all-features"],
|
|
181
|
+
help="Drop metadata for all features in the store",
|
|
182
|
+
),
|
|
183
|
+
] = False,
|
|
184
|
+
confirm: Annotated[
|
|
185
|
+
bool,
|
|
186
|
+
cyclopts.Parameter(
|
|
187
|
+
name=["--confirm"],
|
|
188
|
+
help="Confirm the drop operation (required to prevent accidental deletion)",
|
|
189
|
+
),
|
|
190
|
+
] = False,
|
|
191
|
+
):
|
|
192
|
+
"""Drop metadata from a store.
|
|
193
|
+
|
|
194
|
+
Removes metadata for specified features from the store.
|
|
195
|
+
This is a destructive operation and requires --confirm flag.
|
|
196
|
+
|
|
197
|
+
Useful for:
|
|
198
|
+
- Cleaning up test data
|
|
199
|
+
- Re-computing feature metadata from scratch
|
|
200
|
+
- Removing obsolete features
|
|
201
|
+
|
|
202
|
+
Examples:
|
|
203
|
+
# Drop specific feature (requires confirmation)
|
|
204
|
+
$ metaxy metadata drop --feature user_features --confirm
|
|
205
|
+
|
|
206
|
+
# Drop multiple features
|
|
207
|
+
$ metaxy metadata drop --feature user_features --feature customer_features --confirm
|
|
208
|
+
|
|
209
|
+
# Drop all features from specific store
|
|
210
|
+
$ metaxy metadata drop --store dev --all-features --confirm
|
|
211
|
+
"""
|
|
212
|
+
from metaxy.cli.context import get_store
|
|
213
|
+
|
|
214
|
+
# Validate arguments
|
|
215
|
+
if not all_features and not features:
|
|
216
|
+
console.print(
|
|
217
|
+
"[red]Error:[/red] Must specify either --all-features or --feature"
|
|
218
|
+
)
|
|
219
|
+
raise SystemExit(1)
|
|
220
|
+
|
|
221
|
+
if all_features and features:
|
|
222
|
+
console.print(
|
|
223
|
+
"[red]Error:[/red] Cannot specify both --all-features and --feature"
|
|
224
|
+
)
|
|
225
|
+
raise SystemExit(1)
|
|
226
|
+
|
|
227
|
+
if not confirm:
|
|
228
|
+
console.print(
|
|
229
|
+
"[red]Error:[/red] This is a destructive operation. Must specify --confirm flag."
|
|
230
|
+
)
|
|
231
|
+
raise SystemExit(1)
|
|
232
|
+
|
|
233
|
+
# Parse feature keys
|
|
234
|
+
feature_keys: list[FeatureKey] = []
|
|
235
|
+
if features:
|
|
236
|
+
for feature_str in features:
|
|
237
|
+
# Parse feature key (supports both "feature" and "part1/part2/..." formats)
|
|
238
|
+
if "/" in feature_str:
|
|
239
|
+
parts = feature_str.split("/")
|
|
240
|
+
feature_keys.append(FeatureKey(parts))
|
|
241
|
+
else:
|
|
242
|
+
# Single-part key
|
|
243
|
+
feature_keys.append(FeatureKey([feature_str]))
|
|
244
|
+
|
|
245
|
+
# Get store
|
|
246
|
+
metadata_store = get_store(store)
|
|
247
|
+
|
|
248
|
+
with metadata_store:
|
|
249
|
+
# If all_features, get all feature keys from store
|
|
250
|
+
if all_features:
|
|
251
|
+
# Get all features that have metadata in the store
|
|
252
|
+
feature_keys = metadata_store.list_features(include_fallback=False)
|
|
253
|
+
|
|
254
|
+
console.print(
|
|
255
|
+
f"\n[bold]Dropping metadata for {len(feature_keys)} feature(s)...[/bold]\n"
|
|
256
|
+
)
|
|
257
|
+
|
|
258
|
+
dropped_count = 0
|
|
259
|
+
for feature_key in feature_keys:
|
|
260
|
+
try:
|
|
261
|
+
metadata_store.drop_feature_metadata(feature_key)
|
|
262
|
+
console.print(f"[green]✓[/green] Dropped: {feature_key.to_string()}")
|
|
263
|
+
dropped_count += 1
|
|
264
|
+
except Exception as e:
|
|
265
|
+
console.print(
|
|
266
|
+
f"[red]✗[/red] Failed to drop {feature_key.to_string()}: {e}"
|
|
267
|
+
)
|
|
268
|
+
|
|
269
|
+
console.print(
|
|
270
|
+
f"\n[green]✓[/green] Drop complete: {dropped_count} feature(s) dropped"
|
|
271
|
+
)
|