From f12ee3eab6d1f67d5154fb8b12b81d2b440e7876 Mon Sep 17 00:00:00 2001 From: Zio Gabber <78922322+Gabrymi93@users.noreply.github.com> Date: Wed, 29 Jul 2026 20:34:18 +0100 Subject: [PATCH 1/2] feat(config): add optional dataset.tags and dataset.category fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Aggiunge i campi opzionali tags (list[str]) e category (str | None) al blocco dataset: del dataset.yml, con propagazione in: - DatasetBlock Pydantic model (shared_models.py) - ToolkitConfig dataclass pubblica (config.py) - load_config() — popolamento da YAML - load_dataset_manifest() — manifest pubblico - dataset_info() — esposizione via MCP server I campi sono backward-compatibili: dataset.yml esistenti continuano a funzionare senza modifiche. tags defaulta a [], category a None. Test: 5 nuovi test in test_config_loading.py (contract + policy), 4 nuovi test in test_dataset_loader.py (pure_unit). --- docs/config-schema.md | 2 + tests/helpers.py | 2 + tests/test_config_loading.py | 71 +++++++++++++++++++++ tests/test_dataset_loader.py | 26 ++++++++ toolkit/core/config.py | 7 +- toolkit/core/config_models/shared_models.py | 4 +- toolkit/core/dataset_loader.py | 2 + toolkit/mcp/schema_ops.py | 2 + 8 files changed, 114 insertions(+), 2 deletions(-) diff --git a/docs/config-schema.md b/docs/config-schema.md index 3b364636..214b9cd0 100644 --- a/docs/config-schema.md +++ b/docs/config-schema.md @@ -25,6 +25,8 @@ I path relativi sono sempre risolti rispetto alla directory che contiene `datase |---|---|---| | `dataset.name` | `string` | nessuno | | `dataset.years` | `list[int]` | nessuno | +| `dataset.tags` | `list[string]` | `[]` | +| `dataset.category` | `string \| null` | `null` | ## raw diff --git a/tests/helpers.py b/tests/helpers.py index b1069755..44689f9c 100644 --- a/tests/helpers.py +++ b/tests/helpers.py @@ -110,6 +110,8 @@ def make_config( source_id=model.dataset.source_id, years=list(model.dataset.years), time_coverage=model.dataset.time_coverage, + tags=list(model.dataset.tags or []), + category=model.dataset.category, _model=model, ) diff --git a/tests/test_config_loading.py b/tests/test_config_loading.py index ba08778a..f21e4798 100644 --- a/tests/test_config_loading.py +++ b/tests/test_config_loading.py @@ -585,3 +585,74 @@ def test_mart_required_tables_explicit_subset(tmp_path: Path): cfg = load_config(yml) assert cfg.mart.required_tables == ["table_a"] + + +@pytest.mark.contract +def test_dataset_tags_default_empty(tmp_path: Path): + """dataset.tags defaults to [] when omitted.""" + yml = tmp_path / "dataset.yml" + _yml(yml) + cfg = load_config(yml) + assert cfg.tags == [] + + +@pytest.mark.contract +def test_dataset_tags_parsed(tmp_path: Path): + """dataset.tags è parsato correttamente da YAML lista.""" + yml = tmp_path / "dataset.yml" + _yml_str( + yml, + "dataset:\n" + " name: demo\n" + " years: [2024]\n" + " tags: [sanità, farmaceutica, spesa-pubblica]\n" + "raw: {}\n" + "clean: {}\n" + "mart: {}", + ) + cfg = load_config(yml) + assert cfg.tags == ["sanità", "farmaceutica", "spesa-pubblica"] + + +@pytest.mark.contract +def test_dataset_category_default_none(tmp_path: Path): + """dataset.category defaults to None when omitted.""" + yml = tmp_path / "dataset.yml" + _yml(yml) + cfg = load_config(yml) + assert cfg.category is None + + +@pytest.mark.contract +def test_dataset_category_parsed(tmp_path: Path): + """dataset.category è parsato correttamente da YAML.""" + yml = tmp_path / "dataset.yml" + _yml_str( + yml, + "dataset:\n name: demo\n years: [2024]\n category: sanità\nraw: {}\nclean: {}\nmart: {}", + ) + cfg = load_config(yml) + assert cfg.category == "sanità" + + +@pytest.mark.policy +def test_dataset_tags_and_category_with_source_id(tmp_path: Path): + """tags e category convivono con source_id e time_coverage.""" + yml = tmp_path / "dataset.yml" + _yml_str( + yml, + "dataset:\n" + " name: demo\n" + " years: [2024]\n" + " source_id: test_source\n" + " tags: [tag1, tag2]\n" + " category: test-category\n" + "raw: {}\n" + "clean: {}\n" + "mart: {}", + ) + cfg = load_config(yml) + assert cfg.dataset == "demo" + assert cfg.source_id == "test_source" + assert cfg.tags == ["tag1", "tag2"] + assert cfg.category == "test-category" diff --git a/tests/test_dataset_loader.py b/tests/test_dataset_loader.py index fd5d3d66..c2396ab5 100644 --- a/tests/test_dataset_loader.py +++ b/tests/test_dataset_loader.py @@ -130,6 +130,32 @@ def test_source_id(self, tmp_path: Path) -> None: result = load_dataset_manifest(tmp_path) assert result["source_id"] == "istat_sdmx" + def test_tags_default_empty(self, tmp_path: Path) -> None: + _write_dataset(tmp_path, {"dataset": {"name": "test"}}) + result = load_dataset_manifest(tmp_path) + assert result["tags"] == [] + + def test_tags_parsed(self, tmp_path: Path) -> None: + _write_dataset( + tmp_path, + {"dataset": {"name": "test", "tags": ["sanità", "farmaceutica"]}}, + ) + result = load_dataset_manifest(tmp_path) + assert result["tags"] == ["sanità", "farmaceutica"] + + def test_category_default_none(self, tmp_path: Path) -> None: + _write_dataset(tmp_path, {"dataset": {"name": "test"}}) + result = load_dataset_manifest(tmp_path) + assert result["category"] is None + + def test_category_parsed(self, tmp_path: Path) -> None: + _write_dataset( + tmp_path, + {"dataset": {"name": "test", "category": "sanità"}}, + ) + result = load_dataset_manifest(tmp_path) + assert result["category"] == "sanità" + def test_empty_years_default(self, tmp_path: Path) -> None: _write_dataset(tmp_path, {"dataset": {"name": "test"}}) result = load_dataset_manifest(tmp_path) diff --git a/toolkit/core/config.py b/toolkit/core/config.py index 35f3d2d6..5b92a575 100644 --- a/toolkit/core/config.py +++ b/toolkit/core/config.py @@ -39,7 +39,10 @@ class ToolkitConfig: time_coverage: TimeCoverage | None # Internal: the typed model (used by typed properties below) - _model: ToolkitConfigModel = field(repr=False, compare=False) + _model: ToolkitConfigModel + + tags: list[str] = field(default_factory=list) + category: str | None = None # --- Typed accessors --- @@ -116,5 +119,7 @@ def load_config( source_id=model.dataset.source_id, years=list(model.dataset.years), time_coverage=model.dataset.time_coverage, + tags=list(model.dataset.tags or []), + category=model.dataset.category, _model=model, ) diff --git a/toolkit/core/config_models/shared_models.py b/toolkit/core/config_models/shared_models.py index 33ddbc79..688fa402 100644 --- a/toolkit/core/config_models/shared_models.py +++ b/toolkit/core/config_models/shared_models.py @@ -14,7 +14,7 @@ from pathlib import Path from typing import Any, Literal -from pydantic import BaseModel, ConfigDict, field_validator, model_validator +from pydantic import BaseModel, ConfigDict, Field, field_validator, model_validator _SAFE_SQL_IDENTIFIER_RE = r"^[A-Za-z_][A-Za-z0-9_]*$" @@ -104,6 +104,8 @@ class DatasetBlock(BaseModel): years: list[int] source_id: str | None = None time_coverage: TimeCoverage | None = None + tags: list[str] = Field(default_factory=list) + category: str | None = None class SupportDatasetConfig(BaseModel): diff --git a/toolkit/core/dataset_loader.py b/toolkit/core/dataset_loader.py index 4f455102..a772f8d8 100644 --- a/toolkit/core/dataset_loader.py +++ b/toolkit/core/dataset_loader.py @@ -51,6 +51,8 @@ def load_dataset_manifest(path: str | Path) -> dict[str, Any]: "years": ds.get("years") or [], "source_id": ds.get("source_id"), "time_coverage": ds.get("time_coverage"), + "tags": ds.get("tags") or [], + "category": ds.get("category"), } # raw.sources diff --git a/toolkit/mcp/schema_ops.py b/toolkit/mcp/schema_ops.py index 611bec20..7434a614 100644 --- a/toolkit/mcp/schema_ops.py +++ b/toolkit/mcp/schema_ops.py @@ -407,6 +407,8 @@ def dataset_info(config_path: str) -> dict[str, Any]: "has_mart": has_mart, "mart_tables": mart_tables, "support_datasets": support_list, + "tags": list(cfg.tags) if hasattr(cfg, "tags") else [], + "category": cfg.category if hasattr(cfg, "category") else None, } From ed48fbf59e5d5559d0233d09ba0ca02032b9cd51 Mon Sep 17 00:00:00 2001 From: Zio Gabber <78922322+Gabrymi93@users.noreply.github.com> Date: Wed, 29 Jul 2026 20:59:34 +0100 Subject: [PATCH 2/2] fix(mcp): add source_id to dataset_info() output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allinea dataset_info() con tags/category — espone anche source_id per coerenza nei metadati del dataset via MCP. --- toolkit/mcp/schema_ops.py | 1 + 1 file changed, 1 insertion(+) diff --git a/toolkit/mcp/schema_ops.py b/toolkit/mcp/schema_ops.py index 7434a614..1faabc0d 100644 --- a/toolkit/mcp/schema_ops.py +++ b/toolkit/mcp/schema_ops.py @@ -399,6 +399,7 @@ def dataset_info(config_path: str) -> dict[str, Any]: return { "dataset": cfg.dataset if hasattr(cfg, "dataset") else None, "config_path": str(config), + "source_id": cfg.source_id if hasattr(cfg, "source_id") else None, "years": list(cfg.years) if hasattr(cfg, "years") else [], "time_coverage": time_cov, "source_urls": source_urls,