|
15 | 15 |
|
16 | 16 | import argparse |
17 | 17 | import json |
| 18 | +from pathlib import Path |
18 | 19 | from typing import Any |
19 | 20 |
|
20 | 21 | import pandas as pd |
@@ -153,6 +154,42 @@ def cmd_trust(args: argparse.Namespace) -> int: |
153 | 154 | return 0 |
154 | 155 |
|
155 | 156 |
|
| 157 | +def cmd_quality_ops(args: argparse.Namespace) -> int: |
| 158 | + from ..findings import findings_from_dict |
| 159 | + from ..integrations.quality_ops import export_quality_ops |
| 160 | + |
| 161 | + with open(args.input, encoding="utf-8") as fh: |
| 162 | + report_dict = json.load(fh) |
| 163 | + findings = findings_from_dict(report_dict) |
| 164 | + |
| 165 | + df = _read_frame(args.data, None) if args.data else None |
| 166 | + model_name = args.model_name or Path(args.input).stem |
| 167 | + suite_name = args.suite_name or f"{model_name}_suite" |
| 168 | + |
| 169 | + result = export_quality_ops( |
| 170 | + findings, |
| 171 | + model_name=model_name, |
| 172 | + suite_name=suite_name, |
| 173 | + dbt_path=args.dbt, |
| 174 | + gx_path=args.gx, |
| 175 | + exception_table_path=args.exceptions, |
| 176 | + df=df, |
| 177 | + include_pii=args.include_pii, |
| 178 | + exceptions_format=args.exceptions_format, |
| 179 | + ) |
| 180 | + if args.lineage: |
| 181 | + with open(args.lineage, "w", encoding="utf-8") as fh: |
| 182 | + json.dump(result.lineage_event, fh, indent=2, default=str) |
| 183 | + if not args.quiet: |
| 184 | + print(f"freshdata quality-ops: {len(result.findings)} finding(s)") |
| 185 | + for label, dest in (("dbt", result.dbt_path), ("gx", result.gx_path), |
| 186 | + ("exceptions", result.exception_table_path), |
| 187 | + ("lineage", args.lineage)): |
| 188 | + if dest: |
| 189 | + print(f" {label}: {dest}") |
| 190 | + return 0 |
| 191 | + |
| 192 | + |
156 | 193 | def build_parser() -> argparse.ArgumentParser: |
157 | 194 | parser = argparse.ArgumentParser(prog="freshdata", description="freshdata enterprise CLI") |
158 | 195 | subparsers = parser.add_subparsers(dest="command", required=True) |
@@ -190,6 +227,28 @@ def build_parser() -> argparse.ArgumentParser: |
190 | 227 | help="exit non-zero if the trust score is below this") |
191 | 228 | trust.set_defaults(func=cmd_trust) |
192 | 229 |
|
| 230 | + qops = subparsers.add_parser( |
| 231 | + "quality-ops", |
| 232 | + help="export findings from a report.json to dbt/GX/exception/lineage artifacts", |
| 233 | + ) |
| 234 | + qops.add_argument("input", help="path to a freshdata report JSON (CleanReport.to_dict)") |
| 235 | + qops.add_argument("--dbt", metavar="schema.yml", help="write dbt generic tests YAML here") |
| 236 | + qops.add_argument("--gx", metavar="suite.json", help="write a Great Expectations suite here") |
| 237 | + qops.add_argument("--exceptions", metavar="PATH", |
| 238 | + help="write an exception table here (.csv/.parquet/.duckdb)") |
| 239 | + qops.add_argument("--exceptions-format", choices=("csv", "parquet", "duckdb"), |
| 240 | + help="exception-table format (else inferred from the extension)") |
| 241 | + qops.add_argument("--lineage", metavar="lineage.json", |
| 242 | + help="write the OpenLineage event (with artifact facets) here") |
| 243 | + qops.add_argument("--data", metavar="PATH", |
| 244 | + help="optional source file to enrich exception observed_values") |
| 245 | + qops.add_argument("--model-name", help="dbt model name (default: report filename stem)") |
| 246 | + qops.add_argument("--suite-name", help="GX suite name (default: <model>_suite)") |
| 247 | + qops.add_argument("--include-pii", action="store_true", |
| 248 | + help="reveal observed values in the exception table (default: redacted)") |
| 249 | + qops.add_argument("--quiet", action="store_true") |
| 250 | + qops.set_defaults(func=cmd_quality_ops) |
| 251 | + |
193 | 252 | from ..streaming._cli import add_stream_subparsers |
194 | 253 |
|
195 | 254 | add_stream_subparsers(subparsers) |
|
0 commit comments