From 91b654c1bb43772607f12f36e9a41e99a2e44d35 Mon Sep 17 00:00:00 2001 From: jiatolentino Date: Tue, 16 Jun 2026 12:28:36 +0800 Subject: [PATCH] feat: write split db-report downloads as a zip --- src/datamasque_cli/commands/discovery.py | 22 ++++++++++++++++-- tests/commands/test_discovery.py | 29 +++++++++++++++++++++++- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/src/datamasque_cli/commands/discovery.py b/src/datamasque_cli/commands/discovery.py index 9db83af..5420b1d 100644 --- a/src/datamasque_cli/commands/discovery.py +++ b/src/datamasque_cli/commands/discovery.py @@ -105,12 +105,30 @@ def sdd_report( @app.command("db-report") def db_discovery_report( run_id: int = typer.Argument(help="Run ID"), - output: Path | None = typer.Option(None, "--output", "-o", help="Write CSV to this path"), + output: Path | None = typer.Option(None, "--output", "-o", help="Write CSV (or zip) to this path"), profile: str | None = typer.Option(None, "--profile", "-p", help="Profile to use"), ) -> None: - """Download database discovery report (CSV) for a run.""" + """Download database discovery report for a run. + + Reports within Excel's row limit download as a single CSV. Larger reports are split + server-side into a zip of numbered CSV parts, written out as a binary `.zip`; that case + requires `-o`, since a zip can't be streamed to stdout. + """ client = get_client(profile) report = client.get_db_discovery_result_report(RunId(run_id)) + + if isinstance(report, bytes): + if output is None: + abort( + "This report was split into a zip and can't be written to stdout.", + code=ErrorCode.INVALID_INPUT, + hint=f"Re-run with -o .zip (e.g. -o discovery_report_{run_id}.zip).", + ) + target = output if output.suffix.lower() == ".zip" else output.with_suffix(".zip") + target.write_bytes(report) + print_success(f"Database discovery report (split, zip) written to {target}") + return + _write_or_echo(report, output, "Database discovery report") diff --git a/tests/commands/test_discovery.py b/tests/commands/test_discovery.py index c35218b..7a3d077 100644 --- a/tests/commands/test_discovery.py +++ b/tests/commands/test_discovery.py @@ -37,7 +37,7 @@ def test_sdd_report_echoes_to_stdout_without_output(mock_get_client: MagicMock, @patch(f"{MODULE}.get_client") -def test_db_report_writes_to_output_file(mock_get_client: MagicMock, runner: CliRunner, tmp_path: Path) -> None: +def test_db_report_writes_csv_to_output_file(mock_get_client: MagicMock, runner: CliRunner, tmp_path: Path) -> None: client = MagicMock() mock_get_client.return_value = client client.get_db_discovery_result_report.return_value = "header\nrow1\n" @@ -49,6 +49,33 @@ def test_db_report_writes_to_output_file(mock_get_client: MagicMock, runner: Cli assert out.read_text() == "header\nrow1\n" +@patch(f"{MODULE}.get_client") +def test_db_report_writes_zip_when_split(mock_get_client: MagicMock, runner: CliRunner, tmp_path: Path) -> None: + client = MagicMock() + mock_get_client.return_value = client + zip_bytes = b"PK\x03\x04 fake zip bytes" + client.get_db_discovery_result_report.return_value = zip_bytes + + out = tmp_path / "db.csv" + result = runner.invoke(app, ["discover", "db-report", "42", "--output", str(out)]) + + assert result.exit_code == 0 + assert not out.exists() + assert (tmp_path / "db.zip").read_bytes() == zip_bytes + + +@patch(f"{MODULE}.get_client") +def test_db_report_split_without_output_aborts(mock_get_client: MagicMock, runner: CliRunner) -> None: + client = MagicMock() + mock_get_client.return_value = client + client.get_db_discovery_result_report.return_value = b"PK\x03\x04 fake zip bytes" + + result = runner.invoke(app, ["discover", "db-report", "42"]) + + assert result.exit_code != 0 + assert "-o" in result.stderr + + @patch(f"{MODULE}.get_client") def test_file_report_writes_json_to_output(mock_get_client: MagicMock, runner: CliRunner, tmp_path: Path) -> None: client = MagicMock()