Repository navigation
Expand file tree
/
Copy pathmlflow_cmd.py
More file actions
347 lines (295 loc) · 10.6 KB
/
Copy pathmlflow_cmd.py
File metadata and controls
347 lines (295 loc) · 10.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
"""CLI commands for the Feast–MLflow DataSource integration."""
from __future__ import annotations
import json
import click
@click.group(name="mlflow")
def mlflow_cmd():
"""MLflow integration utilities (dataset sync, source validation)."""
pass
def _require_mlflow() -> None:
try:
import mlflow # noqa: F401
except ImportError as e:
raise click.ClickException(
"The 'mlflow' package is required. Install the 'feast[mlflow]' extra in your environment."
) from e
def _resolve_tracking_uri(store) -> str | None:
"""Read the MLflow tracking URI from store config or environment."""
from feast.mlflow_integration.config import resolve_tracking_uri
mlflow_cfg = getattr(store.config, "mlflow", None)
if mlflow_cfg is not None and hasattr(mlflow_cfg, "get_tracking_uri"):
return mlflow_cfg.get_tracking_uri()
return resolve_tracking_uri(None)
def _print_sync_result(result) -> None:
click.echo("\nSync results:")
click.echo(f" Records fetched: {result.records_fetched}")
click.echo(f" Records ingested: {result.records_ingested}")
click.echo(f" New records: {result.new_records}")
if result.errors:
click.echo(f"\n Errors ({len(result.errors)}):")
for err in result.errors:
click.echo(f" - {err}", err=True)
raise SystemExit(1)
@mlflow_cmd.command("sync-dataset")
@click.option(
"--feature-view",
default=None,
help=(
"Target FeatureView/LabelView. When omitted, syncs all views whose "
"source is MlflowDatasetSource."
),
)
@click.option(
"--source",
default=None,
help="MLflow GenAI dataset name override (defaults to MlflowDatasetSource).",
)
@click.option(
"--full-refresh/--incremental",
default=False,
help="Full refresh re-syncs all records; incremental only syncs new/updated.",
)
@click.option(
"--field-mapping",
default=None,
help="Path to a JSON file with field mapping overrides.",
)
@click.option(
"--batch-size",
default=None,
type=int,
help="Number of rows to write per batch.",
)
@click.option(
"--dry-run/--no-dry-run",
default=False,
help="Fetch and flatten without writing to stores.",
)
@click.pass_context
def sync_dataset_cmd(
ctx, feature_view, source, full_refresh, field_mapping, batch_size, dry_run
):
"""Sync an MLflow GenAI Dataset into a Feast FeatureView/LabelView."""
_require_mlflow()
from feast.mlflow_integration.dataset_sync import (
sync_all_mlflow_dataset_sources,
sync_mlflow_dataset_to_feast,
)
from feast.repo_operations import create_feature_store
store = create_feature_store(ctx)
tracking_uri = _resolve_tracking_uri(store)
mapping = None
if field_mapping:
with open(field_mapping) as f:
mapping = json.load(f)
if tracking_uri:
click.echo(f" MLflow tracking URI: {tracking_uri}")
if dry_run:
click.echo(" DRY RUN — no data will be written.")
if feature_view is None:
click.echo(
f"Syncing all MlflowDatasetSource views "
f"({'full refresh' if full_refresh else 'incremental'})"
)
results = sync_all_mlflow_dataset_sources(
store,
incremental=not full_refresh,
batch_size=batch_size,
dry_run=dry_run,
)
if not results:
click.echo("No FeatureViews/LabelViews with MlflowDatasetSource found.")
return
any_errors = False
for name, result in results.items():
click.echo(f"\n[{name}]")
click.echo(f" Records fetched: {result.records_fetched}")
click.echo(f" Records ingested: {result.records_ingested}")
if result.errors:
any_errors = True
for err in result.errors:
click.echo(f" - {err}", err=True)
if any_errors:
raise SystemExit(1)
return
click.echo(
f"Syncing MLflow dataset → FeatureView '{feature_view}' "
f"({'full refresh' if full_refresh else 'incremental'})"
)
result = sync_mlflow_dataset_to_feast(
store=store,
feature_view_name=feature_view,
dataset_name=source,
field_mapping=mapping,
tracking_uri=tracking_uri,
incremental=not full_refresh,
batch_size=batch_size,
dry_run=dry_run,
)
_print_sync_result(result)
@mlflow_cmd.command("preview-dataset")
@click.option(
"--source",
required=True,
help="MLflow GenAI dataset name.",
)
@click.option(
"--limit",
default=5,
type=int,
help="Number of records to preview (default: 5).",
)
@click.option(
"--field-mapping",
default=None,
help="Path to a JSON file with field mapping overrides.",
)
@click.pass_context
def preview_dataset_cmd(ctx, source, limit, field_mapping):
"""Preview flattened records from an MLflow GenAI Dataset."""
_require_mlflow()
from feast.infra.data_sources.mlflow.auth import (
mlflow_request_scope,
resolve_mlflow_token,
)
from feast.mlflow_integration.dataset_sync import (
_fetch_dataset_with_retry,
flatten_mlflow_dataset_df,
)
from feast.repo_operations import create_feature_store
store = create_feature_store(ctx)
tracking_uri = _resolve_tracking_uri(store)
mapping = None
if field_mapping:
with open(field_mapping) as f:
mapping = json.load(f)
token = resolve_mlflow_token()
click.echo(f"Fetching dataset '{source}'...")
with mlflow_request_scope(token, tracking_uri):
dataset = _fetch_dataset_with_retry(source)
if dataset is None:
raise click.ClickException(f"Failed to fetch MLflow dataset '{source}'.")
df = dataset.to_df()
click.echo(f" Total records: {len(df)}")
if df.empty:
click.echo(" Dataset is empty.")
return
df = flatten_mlflow_dataset_df(df, field_mapping=mapping)
preview = df.head(limit)
click.echo(f"\nFlattened preview ({min(limit, len(df))} rows):")
click.echo(preview.to_string(index=False))
@mlflow_cmd.command("validate-source")
@click.argument("feature_view_name")
@click.pass_context
def validate_source_cmd(ctx, feature_view_name):
"""Validate that an MlflowDatasetSource is reachable and schema-compatible.
Checks that the MLflow artifact or GenAI dataset referenced by the
FeatureView's source exists, has a supported format, and that its
schema matches the FeatureView's expected features.
"""
_require_mlflow()
from feast.infra.data_sources.mlflow.mlflow_dataset_source import (
MlflowDatasetSource,
)
from feast.repo_operations import create_feature_store
store = create_feature_store(ctx)
try:
fv = store.get_feature_view(feature_view_name)
except Exception as e:
raise click.ClickException(f"FeatureView '{feature_view_name}' not found: {e}")
source = fv.batch_source
if not isinstance(source, MlflowDatasetSource):
raise click.ClickException(
f"FeatureView '{feature_view_name}' is not backed by an "
f"MlflowDatasetSource (source type: {type(source).__name__})"
)
click.echo(f"Validating MlflowDatasetSource for '{feature_view_name}'...")
if source.is_genai_mode:
click.echo(
f" Mode: GenAI Dataset "
f"(name={source.dataset_name}, id={source.dataset_id})"
)
else:
click.echo(
f" Mode: Artifact "
f"(run_id={source.run_id}, path={source.artifact_path}, "
f"format={source.artifact_format})"
)
tracking_uri = _resolve_tracking_uri(store)
if tracking_uri:
click.echo(f" Tracking URI: {tracking_uri}")
errors = []
cols = []
try:
source.validate(store.config)
click.echo(" Config validation: PASS")
except Exception as e:
errors.append(f"Config validation: {e}")
click.echo(f" Config validation: FAIL — {e}")
try:
cols = list(source.get_table_column_names_and_types(store.config))
click.echo(f" Schema introspection: PASS ({len(cols)} columns)")
for col_name, col_type in cols:
click.echo(f" {col_name}: {col_type}")
except Exception as e:
errors.append(f"Schema introspection: {e}")
click.echo(f" Schema introspection: FAIL — {e}")
if cols:
fv_features = [f.name for f in fv.features]
source_cols = {c[0] for c in cols}
missing = [f for f in fv_features if f not in source_cols]
if missing:
errors.append(f"Missing columns in source: {missing}")
click.echo(f" Schema match: FAIL — missing columns: {missing}")
else:
click.echo(" Schema match: PASS")
if errors:
click.echo(f"\nValidation FAILED with {len(errors)} error(s).")
raise SystemExit(1)
else:
click.echo("\nValidation PASSED.")
@mlflow_cmd.command("list-sources")
@click.pass_context
def list_sources_cmd(ctx):
"""List all FeatureViews backed by MlflowDatasetSource."""
from feast.repo_operations import create_feature_store
store = create_feature_store(ctx)
try:
from feast.infra.data_sources.mlflow.mlflow_dataset_source import (
MlflowDatasetSource,
)
except ImportError:
raise click.ClickException(
"MlflowDatasetSource is not available. "
"Install feast[mlflow] to use this command."
)
feature_views = store.list_feature_views()
mlflow_views = []
for fv in feature_views:
if isinstance(fv.batch_source, MlflowDatasetSource):
mlflow_views.append(fv)
if not mlflow_views:
click.echo("No FeatureViews backed by MlflowDatasetSource found.")
return
click.echo(
f"Found {len(mlflow_views)} MlflowDatasetSource-backed FeatureView(s):\n"
)
for fv in mlflow_views:
src = fv.batch_source
assert isinstance(src, MlflowDatasetSource)
click.echo(f" {fv.name}:")
if src.is_genai_mode:
click.echo(
f" Mode: GenAI Dataset "
f"(name={src.dataset_name}, id={src.dataset_id})"
)
else:
click.echo(
f" Mode: Artifact "
f"(run_id={src.run_id}, path={src.artifact_path}, "
f"format={src.artifact_format})"
)
if src.tracking_uri:
click.echo(f" Tracking URI: {src.tracking_uri}")
click.echo(f" Batch source: {type(src.batch_source).__name__}")
click.echo()