Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,9 @@ La CI di `dataset-incubator` carica su GCS dopo ogni run validato.
## CLI — comandi essenziali

| Comando | Cosa fa |
|---|---|---|
| `toolkit run` | Esecuzione completa RAW→CLEAN→MART |
|---|---|
| `toolkit run` | Esecuzione completa RAW→CLEAN→MART (default) |
| `toolkit run --batch <file>` | Esegue piú dataset in sequenza |
| `toolkit run raw` | Solo layer RAW |
| `toolkit run clean` | Solo layer CLEAN |
| `toolkit run mart` | Solo layer MART |
Expand Down
4 changes: 2 additions & 2 deletions docs/feature-stability.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Questa matrice serve a chiarire cosa il toolkit considera percorso canonico, cos
| `query` | stable | query SQL su parquet (path o dataset.yml + layer) |
| `parquet_preview(sql=...)` | stable | API core per SQL arbitrario su parquet |
| `run` | stable | percorso canonico |
| `validate all` | stable | percorso canonico |
| `run --batch` | stable | batch esecuzione sequenziale |
| `inspect` | stable | percorso canonico |
| path contract di `dataset.yml` | stable | percorso canonico |
| output `raw/clean/mart/_runs` | stable | percorso canonico |
Expand All @@ -26,7 +26,7 @@ Questa matrice serve a chiarire cosa il toolkit considera percorso canonico, cos

Lettura equivalente a livello package:

- core runtime: `toolkit.raw`, `toolkit.clean`, `toolkit.mart`, `toolkit.scout`, `toolkit.cli` (`run`, `validate`, `inspect`)
- core runtime: `toolkit.raw`, `toolkit.clean`, `toolkit.mart`, `toolkit.scout`, `toolkit.cli` (`run`, `inspect`, `batch` deprecato)
- advanced tooling: `inspect runs --resume`, run parziali, `inspect config --mode profile`, `inspect config --diff`
- compatibility only: config legacy e alias storici

Expand Down
4 changes: 4 additions & 0 deletions smoke/batch.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Batch di smoke test offline — dataset senza dipendenze di rete
# I path sono relativi alla root del toolkit (CWD = toolkit/).
smoke/local_file_csv/dataset.yml
smoke/bdap_ckan_csv/dataset.offline.yml
2 changes: 1 addition & 1 deletion smoke/bdap_ckan_csv/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ toolkit run raw --config dataset.yml
toolkit profile raw --config dataset.yml
toolkit run clean --config dataset.yml
toolkit run mart --config dataset.yml
toolkit inspect summary -c dataset.yml --year 2022
toolkit inspect -c dataset.yml --year 2022
```

## Verifiche attese
Expand Down
2 changes: 1 addition & 1 deletion smoke/bdap_http_csv/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ toolkit run raw --config dataset.yml
toolkit profile raw --config dataset.yml
toolkit run clean --config dataset.yml
toolkit run mart --config dataset.yml
toolkit inspect summary -c dataset.yml --year 2022
toolkit inspect -c dataset.yml --year 2022
```

## Verifiche attese
Expand Down
2 changes: 1 addition & 1 deletion smoke/finanze_http_zip_2023/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Smoke manuale per `http_file` su ZIP pubblico del MEF con extractor `unzip_first
## Comandi

```bash
py -m toolkit.cli.app run all -c smoke/finanze_http_zip_2023/dataset.yml
py -m toolkit.cli.app run -c smoke/finanze_http_zip_2023/dataset.yml
py -m toolkit.cli.app status --dataset finanze_http_zip_2023 --year 2023 --config smoke/finanze_http_zip_2023/dataset.yml
```

Expand Down
2 changes: 1 addition & 1 deletion smoke/istat_sdmx_22_289/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ toolkit run raw --config dataset.yml
toolkit profile raw --config dataset.yml
toolkit run clean --config dataset.yml
toolkit run mart --config dataset.yml
toolkit inspect summary -c dataset.yml --year 2024
toolkit inspect -c dataset.yml --year 2024
```

## Verifiche attese
Expand Down
6 changes: 3 additions & 3 deletions smoke/local_file_csv/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ toolkit run raw --config dataset.yml
toolkit profile raw --config dataset.yml
toolkit run clean --config dataset.yml
toolkit run mart --config dataset.yml
toolkit run all --config dataset.yml
toolkit run full --config dataset.yml
toolkit inspect summary -c dataset.yml --year 2022
toolkit run --config dataset.yml
toolkit run --config dataset.yml
toolkit inspect -c dataset.yml --year 2022
```

## Verifiche attese
Expand Down
2 changes: 1 addition & 1 deletion smoke/zip_http_csv/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ toolkit run raw --config dataset.yml
toolkit profile raw --config dataset.yml
toolkit run clean --config dataset.yml
toolkit run mart --config dataset.yml
toolkit inspect summary -c dataset.yml --year 2022
toolkit inspect -c dataset.yml --year 2022
```

## Verifiche attese
Expand Down
87 changes: 70 additions & 17 deletions tests/test_batch_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ def _write_configs_file(tmp_path: Path, *project_names: str) -> Path:


def test_batch_runs_configs_in_sequence_and_prints_report(tmp_path: Path) -> None:
"""``toolkit run --batch`` esegue config in sequenza e produce report."""
project_a = tmp_path / "project_a"
project_b = tmp_path / "project_b"
_write_batch_project(project_a, "batch_a", 2022)
Expand All @@ -108,7 +109,7 @@ def test_batch_runs_configs_in_sequence_and_prints_report(tmp_path: Path) -> Non
runner = CliRunner()
result = runner.invoke(
app,
["batch", "--configs", str(configs_file), "--step", "all"],
["run", "--batch", str(configs_file)],
catch_exceptions=False,
)

Expand All @@ -130,15 +131,15 @@ def test_batch_runs_configs_in_sequence_and_prints_report(tmp_path: Path) -> Non


def test_batch_smoke_flag(tmp_path: Path) -> None:
"""--smoke usa root/smoke/ come output e sample_rows=1000."""
"""``toolkit run --batch --smoke`` usa root/smoke/ come output."""
project = tmp_path / "project"
_write_batch_project(project, "batch_smoke", 2023)
configs_file = _write_configs_file(tmp_path, "project")

runner = CliRunner()
result = runner.invoke(
app,
["batch", "--configs", str(configs_file), "--step", "all", "--smoke"],
["run", "--batch", str(configs_file), "--smoke"],
catch_exceptions=False,
)

Expand Down Expand Up @@ -170,8 +171,7 @@ def test_batch_smoke_flag(tmp_path: Path) -> None:


def test_batch_dry_run_flag(tmp_path: Path) -> None:
"""--dry-run stampa il piano ma non crea file (usa --step raw per evitare
la validazione SQL dry-run che ha una limitazione DuckDB con alias)."""
"""Backward compat: ``toolkit batch --step raw --dry-run`` (non full SQL)."""
project = tmp_path / "project"
_write_batch_project(project, "batch_dry", 2023)
configs_file = _write_configs_file(tmp_path, "project")
Expand All @@ -193,29 +193,29 @@ def test_batch_dry_run_flag(tmp_path: Path) -> None:


def test_batch_json_output(tmp_path: Path) -> None:
"""--json produce output JSON puro su stdout (log silenziato)."""
"""``toolkit run --batch --json`` produce output JSON puro su stdout."""
project = tmp_path / "project"
_write_batch_project(project, "batch_json", 2023)
configs_file = _write_configs_file(tmp_path, "project")

runner = CliRunner()
result = runner.invoke(
app,
["batch", "--configs", str(configs_file), "--step", "all", "--json"],
["run", "--batch", str(configs_file), "--json"],
catch_exceptions=False,
)

assert result.exit_code == 0
# stdout deve essere JSON puro, parsabile direttamente
report = json.loads(result.output)
report = json.loads(result.stdout)
assert report["summary"]["total"] == 1
assert report["summary"]["passed"] == 1
assert report["rows"][0]["dataset"] == "batch_json"
assert report["rows"][0]["status"] == "SUCCESS"


def test_batch_step_probe(tmp_path: Path) -> None:
"""--step probe esegue probe (salta local_file) e produce report JSON."""
"""Backward compat: ``toolkit batch --step probe`` funziona ancora (deprecato)."""
project = tmp_path / "project"
_write_batch_project(project, "batch_probe", 2023)
configs_file = _write_configs_file(tmp_path, "project")
Expand All @@ -228,13 +228,14 @@ def test_batch_step_probe(tmp_path: Path) -> None:
)

assert result.exit_code == 0
assert "deprecato" in result.stderr
assert "Batch Report" in result.output
assert "batch_probe" in result.output
assert "SUCCESS" in result.output


def test_batch_step_probe_json_output(tmp_path: Path) -> None:
"""--step probe --json produce JSON valido."""
"""Backward compat: ``toolkit batch --step probe --json``."""
project = tmp_path / "project"
_write_batch_project(project, "batch_probe_json", 2023)
configs_file = _write_configs_file(tmp_path, "project")
Expand All @@ -247,7 +248,7 @@ def test_batch_step_probe_json_output(tmp_path: Path) -> None:
)

assert result.exit_code == 0
report = json.loads(result.output)
report = json.loads(result.stdout)
assert report["summary"]["total"] == 1
assert report["summary"]["passed"] == 1
assert report["rows"][0]["dataset"] == "batch_probe_json"
Expand All @@ -256,7 +257,7 @@ def test_batch_step_probe_json_output(tmp_path: Path) -> None:


def test_batch_dry_run_with_json(tmp_path: Path) -> None:
"""--dry-run --json: stdout JSON puro (execution plan silenziato)."""
"""Backward compat: ``toolkit batch --step raw --dry-run --json``."""
project = tmp_path / "project"
_write_batch_project(project, "batch_dry_json", 2023)
configs_file = _write_configs_file(tmp_path, "project")
Expand All @@ -269,8 +270,7 @@ def test_batch_dry_run_with_json(tmp_path: Path) -> None:
)

assert result.exit_code == 0
# stdout deve essere JSON puro, parsabile direttamente
report = json.loads(result.output)
report = json.loads(result.stdout)
assert report["summary"]["total"] == 1
assert report["summary"]["passed"] == 1
assert report["rows"][0]["dataset"] == "batch_dry_json"
Expand All @@ -283,7 +283,7 @@ def test_batch_dry_run_with_json(tmp_path: Path) -> None:

@pytest.mark.policy
def test_batch_step_probe_dry_run_reports_dry_run(tmp_path: Path) -> None:
"""--step probe --dry-run deve riportare DRY_RUN (non SUCCESS)."""
"""Backward compat: ``toolkit batch --step probe --dry-run --json``."""
project = tmp_path / "project"
_write_batch_project(project, "batch_probe_dry", 2023)
configs_file = _write_configs_file(tmp_path, "project")
Expand All @@ -296,15 +296,15 @@ def test_batch_step_probe_dry_run_reports_dry_run(tmp_path: Path) -> None:
)

assert result.exit_code == 0
report = json.loads(result.output)
report = json.loads(result.stdout)
assert report["summary"]["total"] == 1
assert report["summary"]["passed"] == 1
assert report["rows"][0]["status"] == "DRY_RUN"


@pytest.mark.policy
def test_batch_step_raw_dry_run_reuses_runner_across_configs(tmp_path: Path) -> None:
"""Batch --step raw --dry-run processa piu config e produce report."""
"""Backward compat: ``toolkit batch --step raw --dry-run``."""
project_a = tmp_path / "proj_a"
_write_batch_project(project_a, "batch_raw_a", 2023)
project_b = tmp_path / "proj_b"
Expand All @@ -323,3 +323,56 @@ def test_batch_step_raw_dry_run_reuses_runner_across_configs(tmp_path: Path) ->
assert result.exit_code == 0
assert "batch_raw_a" in result.output
assert "batch_raw_b" in result.output


@pytest.mark.contract
def test_run_batch_multi_config_dry_run(tmp_path: Path) -> None:
"""``toolkit run --batch`` con piu' config e --dry-run."""
project_a = tmp_path / "proj_a"
_write_batch_project(project_a, "run_batch_a", 2023)
project_b = tmp_path / "proj_b"
_write_batch_project(project_b, "run_batch_b", 2024)

configs_file = tmp_path / "configs.txt"
configs_file.write_text(f"{project_a}/dataset.yml\n{project_b}/dataset.yml\n", encoding="utf-8")

runner = CliRunner()
result = runner.invoke(
app,
["run", "--batch", str(configs_file), "--dry-run"],
)

# --dry-run in batch con step="all" fa SQL validation che può fallire
# su clean.sql con alias DuckDB. Verifichiamo che processi entrambi
# i config (anche se fallisce per limitazione SQL dry-run)
assert "run_batch_a" in result.output
assert "run_batch_b" in result.output


@pytest.mark.contract
def test_run_batch_end_to_end(tmp_path: Path) -> None:
"""``toolkit run --batch`` end-to-end: esegue, produce output, report."""
project_a = tmp_path / "proj_a"
_write_batch_project(project_a, "e2e_a", 2022)
project_b = tmp_path / "proj_b"
_write_batch_project(project_b, "e2e_b", 2023)

configs_file = tmp_path / "batch.txt"
configs_file.write_text(f"{project_a}/dataset.yml\n{project_b}/dataset.yml\n", encoding="utf-8")

runner = CliRunner()
result = runner.invoke(
app,
["run", "--batch", str(configs_file)],
catch_exceptions=False,
)

assert result.exit_code == 0, result.output
assert "Batch Report" in result.output
assert "e2e_a" in result.output
assert "e2e_b" in result.output
assert "SUCCESS" in result.output

# Verifica output fisici
assert (project_a / "out" / "data" / "mart" / "e2e_a" / "2022" / "mart_totali.parquet").exists()
assert (project_b / "out" / "data" / "mart" / "e2e_b" / "2023" / "mart_totali.parquet").exists()
13 changes: 2 additions & 11 deletions tests/test_cli_path_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,17 +91,8 @@ def test_cli_commands_use_dataset_yml_dir_as_path_base(tmp_path: Path, monkeypat
)
assert run_result.exit_code == 0, run_result.output

validate_result = runner.invoke(
app,
[
"validate",
"all",
"--config",
str(config_path),
],
)
assert validate_result.exit_code == 0, validate_result.output

# Validazione gia' eseguita internamente da run — il comando
# 'validate' e' stato rimosso (coperto da run --dry-run)
profile_result = runner.invoke(
app,
[
Expand Down
25 changes: 13 additions & 12 deletions tests/test_cmd_batch.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,11 @@
"""Tests for toolkit/cli/cmd_batch.py — helper functions and failure cases."""
"""Tests for batch helper functions."""

from pathlib import Path

import pytest

from toolkit.cli.cmd_batch import _format_duration, _format_years, _read_config_list
from toolkit.cli._batch_helpers import format_duration, format_years
from toolkit.cli.cmd_run import _read_config_list

pytestmark = pytest.mark.pure_unit

Expand Down Expand Up @@ -145,33 +146,33 @@ def test_blank_lines_and_spaces_skipped(self, tmp_path: pytest.TempPathFactory)

class TestFormatYears:
def test_none(self) -> None:
assert _format_years(None) == "-"
assert format_years(None) == "-"

def test_empty_list(self) -> None:
assert _format_years([]) == "-"
assert format_years([]) == "-"

def test_single_year(self) -> None:
assert _format_years([2023]) == "2023"
assert format_years([2023]) == "2023"

def test_multiple_years(self) -> None:
assert _format_years([2021, 2022, 2023]) == "2021,2022,2023"
assert format_years([2021, 2022, 2023]) == "2021,2022,2023"

def test_returns_string(self) -> None:
assert isinstance(_format_years([2020]), str)
assert isinstance(format_years([2020]), str)


class TestFormatDuration:
def test_none_returns_dash(self) -> None:
assert _format_duration(None) == "-"
assert format_duration(None) == "-"

def test_seconds_formatted(self) -> None:
assert _format_duration(1.234) == "1.234s"
assert format_duration(1.234) == "1.234s"

def test_zero(self) -> None:
assert _format_duration(0.0) == "0.000s"
assert format_duration(0.0) == "0.000s"

def test_rounds_to_3_decimals(self) -> None:
assert _format_duration(1.23456789) == "1.235s"
assert format_duration(1.23456789) == "1.235s"

def test_returns_string(self) -> None:
assert isinstance(_format_duration(1.0), str)
assert isinstance(format_duration(1.0), str)
Loading
Loading