From a7463b94738109a831b753a5b600f6609c695673 Mon Sep 17 00:00:00 2001 From: Zio Gabber <78922322+Gabrymi93@users.noreply.github.com> Date: Sun, 4 Oct 2026 16:48:05 +0100 Subject: [PATCH 1/3] =?UTF-8?q?feat(registry):=20semantic=20civic=20keys?= =?UTF-8?q?=20=E2=80=94=20alias=20estesi=20+=20warning=20colonne=20untyped?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - semantic_types.yaml: alias per nomi reali dei dataset Lab (missione/codice_missione, CF OC/MEF/ANAC, catastali, ATECO OC, MIUR, NUTS parent, provincia_cm, beneficiario_partita_iva, ipa variants) - province_code: provincia_cm_codice_istat NON municipality_code - schema_reader: suggest_semantic_type + find_untyped_civic_keys; descrizioni/descr_* non suggerite come chiavi - cmd_registry build: WARN non-blocking colonne civic senza semantic_type - tests: test_semantic_civic_keys (pure_unit) Copertura centralizzata nel vocabolario toolkit: non si disperde semantic_type nei dataset.yml dei singoli repo. --- tests/test_semantic_civic_keys.py | 136 +++++++++++++++++++++++++++ toolkit/cli/cmd_registry.py | 26 +++++ toolkit/registry/schema_reader.py | 95 +++++++++++++++++++ toolkit/registry/semantic_types.yaml | 49 ++++++---- 4 files changed, 290 insertions(+), 16 deletions(-) create mode 100644 tests/test_semantic_civic_keys.py diff --git a/tests/test_semantic_civic_keys.py b/tests/test_semantic_civic_keys.py new file mode 100644 index 0000000..100b260 --- /dev/null +++ b/tests/test_semantic_civic_keys.py @@ -0,0 +1,136 @@ +"""Test del suggerimento/censimento colonne civic senza semantic_type. + +Marker: pure_unit (logica pura sul vocabolario e sul catalogo in memoria). +""" + +from __future__ import annotations + +import pytest + +from toolkit.registry.schema_reader import ( + find_untyped_civic_keys, + load_semantic_types, + load_valid_types, + suggest_semantic_type, +) + + +class TestSuggestSemanticType: + @pytest.fixture(autouse=True) + def _vocab(self) -> None: + self.alias_map = load_semantic_types() + self.valid_types = load_valid_types() + + @pytest.mark.pure_unit + def test_alias_missione_is_programma(self) -> None: + assert ( + suggest_semantic_type("missione", self.alias_map, self.valid_types) == "programma_code" + ) + + @pytest.mark.pure_unit + def test_alias_codice_missione_bdap(self) -> None: + assert ( + suggest_semantic_type("codice_missione", self.alias_map, self.valid_types) + == "programma_code" + ) + + @pytest.mark.pure_unit + def test_alias_cf_variants(self) -> None: + assert ( + suggest_semantic_type("soggetto_ricevente_cf", self.alias_map, self.valid_types) + == "fiscal_code" + ) + assert ( + suggest_semantic_type("OC_CODICE_FISCALE_SOGG", self.alias_map, self.valid_types) + == "fiscal_code" + ) + + @pytest.mark.pure_unit + def test_alias_catastale_comune(self) -> None: + assert ( + suggest_semantic_type("codice_catastale_comune", self.alias_map, self.valid_types) + == "cadastral_code" + ) + + @pytest.mark.pure_unit + def test_pattern_municipality(self) -> None: + # pattern: non nel vocabolario ma civic-like + assert ( + suggest_semantic_type("codice_comune_sogg_titolare", self.alias_map, self.valid_types) + == "municipality_code" + ) + + @pytest.mark.pure_unit + def test_unknown_column_returns_none(self) -> None: + assert suggest_semantic_type("descrizione_libera", self.alias_map, self.valid_types) is None + assert suggest_semantic_type("stato_cup", self.alias_map, self.valid_types) is None + + @pytest.mark.pure_unit + def test_description_columns_not_suggested_as_keys(self) -> None: + # descrizioni di chiavi: non sono join key + assert suggest_semantic_type("descr_codice_ateco", self.alias_map, self.valid_types) is None + assert ( + suggest_semantic_type("descrizione_submisura", self.alias_map, self.valid_types) is None + ) + assert ( + suggest_semantic_type("desc_codice_catastale", self.alias_map, self.valid_types) is None + ) + + @pytest.mark.pure_unit + def test_provincia_cm_is_province_not_municipality(self) -> None: + assert ( + suggest_semantic_type("provincia_cm_codice_istat", self.alias_map, self.valid_types) + == "province_code" + ) + + @pytest.mark.pure_unit + def test_piva_beneficiario_is_fiscal(self) -> None: + assert ( + suggest_semantic_type("beneficiario_partita_iva", self.alias_map, self.valid_types) + == "fiscal_code" + ) + + @pytest.mark.pure_unit + def test_empty_name_returns_none(self) -> None: + assert suggest_semantic_type("", self.alias_map, self.valid_types) is None + + +class TestFindUntypedCivicKeys: + @pytest.mark.pure_unit + def test_finds_untyped_and_skips_typed(self) -> None: + catalog = { + "datasets": [ + { + "slug": "demo", + "columns": [ + {"name": "missione", "type": "varchar", "role": "dimension"}, + { + "name": "cup", + "type": "varchar", + "role": "dimension", + "semantic_type": "cup_code", + }, + {"name": "valore", "type": "double", "role": "metric"}, + ], + } + ] + } + findings = find_untyped_civic_keys(catalog) + assert len(findings) == 1 + assert findings[0]["slug"] == "demo" + assert findings[0]["column"] == "missione" + assert findings[0]["suggested"] == "programma_code" + + @pytest.mark.pure_unit + def test_empty_catalog(self) -> None: + assert find_untyped_civic_keys({"datasets": []}) == [] + assert find_untyped_civic_keys({}) == [] + + @pytest.mark.pure_unit + def test_missing_vocab_returns_empty(self, tmp_path) -> None: + catalog = { + "datasets": [ + {"slug": "x", "columns": [{"name": "cf", "type": "varchar", "role": "dimension"}]} + ] + } + assert find_untyped_civic_keys(catalog, tmp_path / "nope.yaml") == [] diff --git a/toolkit/cli/cmd_registry.py b/toolkit/cli/cmd_registry.py index 84b3ca5..4f4069e 100644 --- a/toolkit/cli/cmd_registry.py +++ b/toolkit/cli/cmd_registry.py @@ -135,6 +135,29 @@ def _setup_build( return layout, contract, existing_path +def _warn_untyped_civic_keys(catalog: dict[str, Any], limit: int = 20) -> list[dict[str, Any]]: + """Warning non-blocking: colonne civic senza semantic_type nel catalogo.""" + from toolkit.registry.schema_reader import find_untyped_civic_keys + + findings = find_untyped_civic_keys(catalog) + if not findings: + return [] + typer.echo( + f"WARN: {len(findings)} colonne civic senza semantic_type " + "(aggiungi il tipo in dataset.yml o un alias in semantic_types.yaml)", + err=True, + ) + for item in findings[:limit]: + typer.echo( + f" WARN: {item['slug']}.{item['column']} → suggerito {item['suggested']} " + f"(via {item['via']})", + err=True, + ) + if len(findings) > limit: + typer.echo(f" WARN: ... e altre {len(findings) - limit} colonne", err=True) + return findings + + def registry_build( repo: str = typer.Option(None, "--repo", help="Root del repo (default: CWD)"), prefix: str = typer.Option("", "--prefix", help="Prefisso GCS (es. 'eurostat')"), @@ -186,6 +209,7 @@ def registry_build( f"entities aggiornate: {len(existing['entities']['entities'])} entità, " f"{len(existing['entities']['bridges'])} bridge" ) + _warn_untyped_civic_keys(catalog) if not write: typer.echo("Dry-run: usa --write per scrivere il file.") @@ -238,6 +262,8 @@ def registry_build( f"marts {len(registry['marts'])}, " f"signals {len(registry['signals'])} (repo: {layout.source_repo})" ) + _warn_untyped_civic_keys({"datasets": registry.get("datasets", [])}) + if not write: typer.echo("Dry-run: usa --write per scrivere il file.") return diff --git a/toolkit/registry/schema_reader.py b/toolkit/registry/schema_reader.py index d9a40bf..1b422f5 100644 --- a/toolkit/registry/schema_reader.py +++ b/toolkit/registry/schema_reader.py @@ -10,6 +10,7 @@ from __future__ import annotations +import re from pathlib import Path from typing import Any @@ -185,3 +186,97 @@ def latest_clean_columns( if parquet is not None: return parquet_columns(parquet, alias_map), year return None, None + + +# Pattern civic "forti" per colonne che spesso non hanno alias espliciti. +# Solo match di chiavi (non label/status/date): evita falsi positivi. +_CIVIC_KEY_PATTERNS: tuple[tuple[str, str], ...] = ( + (r"(^|_)codice_istat($|_)|comune_codice_istat|cod_comune", "municipality_code"), + (r"codice_catastale|codi_catastale|cod_catastale", "cadastral_code"), + (r"(^|_)cf$|_cf$|^cf_|codice_fiscale|partita_iva|^piva", "fiscal_code"), + (r"^cup$|^codice_cup$|codice_cup_", "cup_code"), + (r"^cig$|^codice_cig$|codice_cig_", "cig_code"), + (r"codice_ipa|codice_ente_ipa", "ipa_code"), + (r"codice_ente_siope|codice_ente_bdap", "siope_code"), + (r"codice_missione|pnrr_missione|^missione$", "programma_code"), + (r"codice_indicatore|COD_INDICATORE", "indicatore_code"), + (r"codice_miur|codice_ente_miur", "miur_code"), + (r"^username$", "sogei_code"), + (r"^COD_ATECO|codice_ateco", "ateco_code"), + (r"codice_istat_regione|regione_codice_istat|codice_regione_istat", "region_code"), + (r"nuts_parent_code", "nuts_code"), + (r"provincia_cm_codice_istat", "province_code"), +) + +# Colonne che sono label/descrizione di una chiave, non la chiave stessa. +# Non suggerire semantic_type civico: il join forte resta sul codice. +_DESC_LABEL_RE = re.compile( + r"(^descr_|^desc_|_descrizione$|^descrizione_|_label$|^label_)", + re.IGNORECASE, +) + + +def suggest_semantic_type( + col_name: str, + alias_map: dict[str, str], + valid_types: set[str], +) -> str | None: + """Suggerisce un semantic_type per una colonna civic-like. + + 1. Alias esatto (case-insensitive) dal vocabolario. + 2. Pattern civic forti (solo se il tipo esiste nel vocabolario). + + Le colonne di descrizione/label (descr_*, descrizione_*, *_label) non + vengono tipizzate come chiavi: sono testo legato alla chiave. + + Returns: + Tipo suggerito o None se non riconosciuto. + """ + if not col_name: + return None + if _DESC_LABEL_RE.search(col_name): + return None + hit = alias_map.get(col_name.lower()) + if hit and hit in valid_types: + return hit + for pattern, suggested in _CIVIC_KEY_PATTERNS: + if suggested in valid_types and re.search(pattern, col_name, re.IGNORECASE): + return suggested + return None + + +def find_untyped_civic_keys( + catalog: dict[str, Any], + semantic_types_path: Path | None = None, +) -> list[dict[str, Any]]: + """Trova colonne civic senza semantic_type in un clean_catalog/registry. + + Non blocca: è un warning di qualità per il registry build. Le colonne + senza tipo che matchano alias/pattern civic meritano un tipo nel + dataset.yml o un alias nel vocabolario. + """ + alias_map = load_semantic_types(semantic_types_path) + valid_types = load_valid_types(semantic_types_path) + if not alias_map and not valid_types: + return [] + + findings: list[dict[str, Any]] = [] + for ds in catalog.get("datasets") or []: + slug = ds.get("slug") or "" + for col in ds.get("columns") or []: + name = col.get("name") or "" + if not name or col.get("semantic_type"): + continue + suggested = suggest_semantic_type(name, alias_map, valid_types) + if not suggested: + continue + via = "alias" if alias_map.get(name.lower()) == suggested else "pattern" + findings.append( + { + "slug": slug, + "column": name, + "suggested": suggested, + "via": via, + } + ) + return findings diff --git a/toolkit/registry/semantic_types.yaml b/toolkit/registry/semantic_types.yaml index 16c587d..79b6ea0 100644 --- a/toolkit/registry/semantic_types.yaml +++ b/toolkit/registry/semantic_types.yaml @@ -27,7 +27,10 @@ types: aliases: [codice_istat, codice_istat_comune, codice_istat_luogo, codice_comune, cod_comune, pro_com, codice_comune_istat, localizzazione_codice_istat, comune_istat, cod_istat_comune, comune_codice, cod_comune_istat, - ente_codice_comune_istat, codice_comune_comune] + ente_codice_comune_istat, codice_comune_comune, + codice_comune_amministrazione, codice_comune_bene, + codice_comune_sogg_titolare, COD_COMUNE_SEDE_SOGG, + comune_codice_istat] hub: hub_slug: comuni_master hub_column: codice_istat @@ -44,7 +47,8 @@ types: description: "Codice catastale Belfiore (es. A010, C466)" entity: Comune aliases: [codice_catastale, cod_catastale, catastale, codice_belfiore, - codice_comune_scuola] + codice_comune_scuola, codice_catastale_comune, codi_catastale, + comune_codice_catastale] hub: hub_slug: comuni_master hub_column: codice_catastale @@ -74,7 +78,8 @@ types: description: "Sigla o codice della provincia" entity: Provincia aliases: [provincia, sigla_provincia, sigla_provincia_pa, sigla_provincia_asl, - sigla_provincia_struttura, codice_provincia, sigla_prov, cod_prov] + sigla_provincia_struttura, codice_provincia, sigla_prov, cod_prov, + provincia_cm_codice_istat, provincia_cm_codice] hub: hub_slug: comuni_master hub_column: sigla_provincia @@ -82,12 +87,14 @@ types: - name: UPPER TRIM expr: "UPPER(TRIM({col}))" weight: 5 - + note: "provincia_cm_codice_istat (SAI/CAS) e' codice provincia, non comune a 6 cifre." region_code: description: "Codice o nome della regione" entity: Regione aliases: [regione, codice_regione, cod_regione, cod_reg, regione_codice, - istat_regione, regione_istat_cod, regione_beneficiario, amministrazione_regione_sede] + istat_regione, regione_istat_cod, regione_beneficiario, + amministrazione_regione_sede, codice_istat_regione, + regione_codice_istat, codice_regione_istat, codice_regione_istat] hub: hub_slug: comuni_master hub_column: regione @@ -99,7 +106,7 @@ types: nuts_code: description: "Codice NUTS Eurostat (es. ITC45)" entity: Provincia (tramite NUTS3) - aliases: [geo, nuts, nuts2, nuts3, nuts_code, codice_nuts] + aliases: [geo, nuts, nuts2, nuts3, nuts_code, codice_nuts, nuts_parent_code] hub: hub_slug: comuni_master hub_column: nuts3_2021 @@ -118,7 +125,13 @@ types: entity: Ente (o Impresa) aliases: [codice_fiscale, codice_fiscale_ente, cf, cf_amministrazione_appaltante, cf_soggetto_attuatore, cf_subappaltante, amministrazione_codice_fiscale, - partita_iva, p_iva, piva, codi_fiscale, codice_fiscale_beneficiario] + partita_iva, p_iva, piva, codi_fiscale, codice_fiscale_beneficiario, + OC_CODICE_FISCALE_SOGG, CODICE_FISCALE_SOGGETTO, codice_fiscale_soggetto, + soggetto_ricevente_cf, soggetto_cedente_cf, amm_cf, societa_cf, + partecipata_codice_fiscale, piva_soggetto_titolare, + piva_codfiscale_sog_titolare, CF_SA_DELEGANTE, CF_SA_DELEGATA, + cf_sa_delegante, cf_sa_delegata, societa_codice_fiscale, + beneficiario_partita_iva, beneficiario_codice_fiscale] hub: hub_slug: comuni_master hub_column: codice_fiscale @@ -128,12 +141,12 @@ types: - name: REPLACE brackets expr: "REPLACE(REPLACE({col}, '[', ''), ']', '')" weight: 15 - note: "Match solo per enti pubblici. CF di imprese (ANAC, RNA) non matchano (0%)." + note: "Match solo per enti pubblici. CF di imprese (ANAC, RNA) non matchano (0%). PIVA e CF sono lo stesso tipo nel vocabolario (fiscal_code); il join a hub PA resta fragile per le imprese." ipa_code: description: "Codice IndicePA dell'ente (es. c_a010)" entity: Ente - aliases: [codice_ipa, ipa, cod_ipa] + aliases: [codice_ipa, ipa, cod_ipa, codice_ente_ipa, codice_istat_ipa] hub: hub_slug: comuni_master hub_column: codice_ipa @@ -143,6 +156,7 @@ types: - name: LOWER expr: "LOWER({col})" weight: 15 + note: "codice_istat_ipa in comuni_master e' il codice ISTAT associato all'ente IPA, non il codice comune primario: tipizzare come ipa_code evita ambiguita con municipality_code." # ═══════════════════════════════════════════════════════════════════════ # DOMINIO APPALTI @@ -206,7 +220,7 @@ types: siope_code: description: "Codice ente SIOPE (9 caratteri)" entity: Ente - aliases: [codice_ente] + aliases: [codice_ente, codice_ente_siope, codice_ente_bdap] bridge: via: bdap_anagrafe_enti on_column: codice_ente_siope @@ -216,7 +230,7 @@ types: miur_code: description: "Codice ente MIUR" entity: Ente - aliases: [codice_ente_miur] + aliases: [codice_ente_miur, codice_miur] bridge: via: bdap_anagrafe_enti on_column: codice_ente_miur @@ -250,7 +264,8 @@ types: ateco_code: description: "Codice ATECO attività economica" entity: Attività economica - aliases: [ateco, codice_ateco, sezione_ateco, ateco_code] + aliases: [ateco, codice_ateco, sezione_ateco, ateco_code, + COD_ATECO, COD_ATECO_SOGG, codice_ateco_sogg] weight: 8 country_code: @@ -319,13 +334,14 @@ types: programma_code: description: "Codice programma operativo (FESR/FSE/PNRR/BDAP)" entity: Programma - aliases: [codice_programma, OC_CODICE_PROGRAMMA, programma, cod_programma] + aliases: [codice_programma, OC_CODICE_PROGRAMMA, programma, cod_programma, + missione, codice_missione, pnrr_missione] bridge: via: opencoesione_progetti_esteso on_column: CUP to: cup_code weight: 15 - note: "In PNRR la missione (M1-M5) funge da programma. In FESR/FSE il codice è OC_CODICE_PROGRAMMA (es. 2021IT16RFPR005). In BDAP è codice_missione+codice_programma." + note: "In PNRR la missione (M1-M7) funge da programma. In FESR/FSE il codice è OC_CODICE_PROGRAMMA (es. 2021IT16RFPR005). In BDAP è codice_missione+codice_programma. Alias missione/codice_missione coprono le colonne reali di pnrr_progetti e bdap_spese_stato." fondo_code: description: "Codice o nome fondo finanziario (FESR, FSE+, FSC, PNRR)" @@ -363,6 +379,7 @@ types: description: "Obiettivo specifico di un programma (descrizione missione/misura)" entity: Obiettivo aliases: [descrizione_missione, descrizione_misura, descrizione_submisura, - OC_DESCRIZIONE_PROGRAMMA, descrizione_obiettivo] + OC_DESCRIZIONE_PROGRAMMA, descrizione_obiettivo, + descrizione_missione_pnrr, descrizione_misura_pnrr] weight: 10 - note: "In PNRR: descrizione_missione/misura/submisura. In FESR/FSE: OC_DESCRIZIONE_PROGRAMMA." + note: "In PNRR: descrizione_missione/misura/submisura. In FESR/FSE: OC_DESCRIZIONE_PROGRAMMA. Le descrizioni sono label testuali del programma, non codici univoci: il join forte resta su missione/OC_CODICE_PROGRAMMA." From 193a4aeb1a41be2421a535b5295e848fc3b89b67 Mon Sep 17 00:00:00 2001 From: Zio Gabber <78922322+Gabrymi93@users.noreply.github.com> Date: Sun, 4 Oct 2026 18:43:36 +0100 Subject: [PATCH 2/3] =?UTF-8?q?fix(registry):=20review=20PR=20#496=20?= =?UTF-8?q?=E2=80=94=20pattern=20conservativi=20+=20test=20CLI=20WARN?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _CIVIC_KEY_PATTERNS: specifici (regione/ipa/provincia) PRIMA di municipality; municipality limitato a contesto comune (niente generico codice_istat) - rimosso pattern username -> sogei_code (troppo aggressivo) - alias duplicate codice_regione_istat rimosse in region_code - WARN copy: solo semantic_types.yaml (copertura centralizzata, no dataset.yml) - test: anti-falso-positivo (codice_istat_ipa -> ipa_code) + CLI path non-blocking (exit 0 + stampa findings) --- tests/test_cli_registry.py | 91 ++++++++++++++++++++++++++++ tests/test_semantic_civic_keys.py | 34 +++++++++++ toolkit/cli/cmd_registry.py | 8 ++- toolkit/registry/schema_reader.py | 27 ++++++--- toolkit/registry/semantic_types.yaml | 2 +- 5 files changed, 150 insertions(+), 12 deletions(-) diff --git a/tests/test_cli_registry.py b/tests/test_cli_registry.py index 7861f49..5862092 100644 --- a/tests/test_cli_registry.py +++ b/tests/test_cli_registry.py @@ -173,3 +173,94 @@ def test_only_entities_fails_without_existing_registry(tmp_path: Path) -> None: ) assert result.exit_code != 0 assert "registry.json non esistente" in result.stderr + + +# --------------------------------------------------------------------------- +# WARN colonne civic senza semantic_type (PR #496) +# --------------------------------------------------------------------------- + + +@pytest.mark.pure_unit +def test_warn_untyped_civic_keys_unit_non_blocking(capsys) -> None: + """_warn_untyped_civic_keys: stampa, ritorna findings, non alza eccezioni.""" + from toolkit.cli.cmd_registry import _warn_untyped_civic_keys + + catalog = { + "datasets": [ + { + "slug": "demo", + "columns": [ + {"name": "missione", "type": "varchar", "role": "dimension"}, + { + "name": "cup", + "type": "varchar", + "role": "dimension", + "semantic_type": "cup_code", + }, + ], + } + ] + } + findings = _warn_untyped_civic_keys(catalog) + assert len(findings) == 1 + assert findings[0]["column"] == "missione" + captured = capsys.readouterr() + assert "colonne civic senza semantic_type" in captured.err + assert "semantic_types.yaml" in captured.err + assert "dataset.yml" not in captured.err # copy allineato all'architettura + + +@pytest.mark.pure_unit +def test_warn_untyped_civic_keys_empty_catalog(capsys) -> None: + from toolkit.cli.cmd_registry import _warn_untyped_civic_keys + + assert _warn_untyped_civic_keys({"datasets": []}) == [] + assert "colonne civic" not in capsys.readouterr().err + + +@pytest.mark.pure_unit +def test_registry_build_warns_untyped_civic_column_non_blocking(tmp_path: Path) -> None: + """Build con colonna civic non tipizzata → exit 0 + WARN (non blocca).""" + from tests.helpers import write_parquet + + ds_dir = tmp_path / "datasets" / "civic-demo" + ds_dir.mkdir(parents=True) + (ds_dir / "dataset.yml").write_text( + """ +root: "../../out" +schema_version: 1 +dataset: + name: "civic_demo" + source_id: "src1" + tags: [test] + category: demo + years: [2024] +registry: + description: "demo civic untyped" +""".strip() + + "\n", + encoding="utf-8", + ) + (ds_dir / "sql").mkdir() + (ds_dir / "sql" / "clean.sql").write_text("SELECT 1", encoding="utf-8") + + out = tmp_path / "out" / "data" / "clean" / "civic_demo" / "2024" + out.mkdir(parents=True) + write_parquet( + out / "civic_demo_2024_clean.parquet", + # nome civic via pattern (non alias): deve restare untyped e generare WARN + "CREATE TABLE t (codice_fiscale_demo VARCHAR, val DOUBLE); " + "INSERT INTO t VALUES ('ABC', 1.0)", + ) + + result = runner.invoke( + app, + ["registry", "build", "--repo", str(tmp_path), "--out", str(tmp_path / "reg")], + obj={}, + ) + # Non-blocking: exit 0 anche se ci sono WARN + assert result.exit_code == 0, result.stdout + result.stderr + combined = result.stdout + result.stderr + assert "colonne civic senza semantic_type" in combined + assert "codice_fiscale_demo" in combined + assert "fiscal_code" in combined diff --git a/tests/test_semantic_civic_keys.py b/tests/test_semantic_civic_keys.py index 100b260..6958315 100644 --- a/tests/test_semantic_civic_keys.py +++ b/tests/test_semantic_civic_keys.py @@ -76,6 +76,40 @@ def test_description_columns_not_suggested_as_keys(self) -> None: suggest_semantic_type("desc_codice_catastale", self.alias_map, self.valid_types) is None ) + @pytest.mark.pure_unit + def test_specific_patterns_before_generic_municipality(self) -> None: + # Review PR #496: il generico codice_istat non deve catturare ipa/regione + assert ( + suggest_semantic_type("codice_istat_ipa", self.alias_map, self.valid_types) + == "ipa_code" + ) + assert ( + suggest_semantic_type("codice_istat_regione", self.alias_map, self.valid_types) + == "region_code" + ) + assert ( + suggest_semantic_type("codice_istat", self.alias_map, self.valid_types) + == "municipality_code" + ) + assert ( + suggest_semantic_type("comune_codice_istat", self.alias_map, self.valid_types) + == "municipality_code" + ) + assert ( + suggest_semantic_type("codice_comune_amministrazione", self.alias_map, self.valid_types) + == "municipality_code" + ) + # ipa_codice_istat: se non in alias, non deve diventare municipality + sug = suggest_semantic_type("ipa_codice_istat", self.alias_map, self.valid_types) + assert sug != "municipality_code" + + @pytest.mark.pure_unit + def test_no_sogei_pattern_for_generic_username(self) -> None: + # Review: ^username$ è troppo aggressivo — nessun pattern sogei_code + from toolkit.registry.schema_reader import _CIVIC_KEY_PATTERNS + + assert not any(s == "sogei_code" for _, s in _CIVIC_KEY_PATTERNS) + @pytest.mark.pure_unit def test_provincia_cm_is_province_not_municipality(self) -> None: assert ( diff --git a/toolkit/cli/cmd_registry.py b/toolkit/cli/cmd_registry.py index 4f4069e..a3edffb 100644 --- a/toolkit/cli/cmd_registry.py +++ b/toolkit/cli/cmd_registry.py @@ -136,7 +136,11 @@ def _setup_build( def _warn_untyped_civic_keys(catalog: dict[str, Any], limit: int = 20) -> list[dict[str, Any]]: - """Warning non-blocking: colonne civic senza semantic_type nel catalogo.""" + """Warning non-blocking: colonne civic senza semantic_type nel catalogo. + + Copertura centralizzata: il fix è un alias in ``semantic_types.yaml``, + non la dichiarazione sparsa nei ``dataset.yml`` dei repo. + """ from toolkit.registry.schema_reader import find_untyped_civic_keys findings = find_untyped_civic_keys(catalog) @@ -144,7 +148,7 @@ def _warn_untyped_civic_keys(catalog: dict[str, Any], limit: int = 20) -> list[d return [] typer.echo( f"WARN: {len(findings)} colonne civic senza semantic_type " - "(aggiungi il tipo in dataset.yml o un alias in semantic_types.yaml)", + "(aggiungi un alias in toolkit semantic_types.yaml — copertura centralizzata)", err=True, ) for item in findings[:limit]: diff --git a/toolkit/registry/schema_reader.py b/toolkit/registry/schema_reader.py index 1b422f5..fe72ce3 100644 --- a/toolkit/registry/schema_reader.py +++ b/toolkit/registry/schema_reader.py @@ -189,23 +189,31 @@ def latest_clean_columns( # Pattern civic "forti" per colonne che spesso non hanno alias espliciti. -# Solo match di chiavi (non label/status/date): evita falsi positivi. +# Ordinati dal più specifico al più generico: regione/ipa/provincia prima +# di municipality, che resta legato al contesto "comune" (mai un generico +# "codice_istat" che catturerebbe ipa/regione/provincia). _CIVIC_KEY_PATTERNS: tuple[tuple[str, str], ...] = ( - (r"(^|_)codice_istat($|_)|comune_codice_istat|cod_comune", "municipality_code"), + # Specifici territoriali (PRIMA del generic comune) + (r"codice_istat_regione|regione_codice_istat|codice_regione_istat", "region_code"), + (r"codice_ipa|codice_ente_ipa|codice_istat_ipa", "ipa_code"), + (r"provincia_cm_codice|codice_provincia_cm", "province_code"), (r"codice_catastale|codi_catastale|cod_catastale", "cadastral_code"), + # Chiavi denaro/progetto (r"(^|_)cf$|_cf$|^cf_|codice_fiscale|partita_iva|^piva", "fiscal_code"), (r"^cup$|^codice_cup$|codice_cup_", "cup_code"), (r"^cig$|^codice_cig$|codice_cig_", "cig_code"), - (r"codice_ipa|codice_ente_ipa", "ipa_code"), (r"codice_ente_siope|codice_ente_bdap", "siope_code"), (r"codice_missione|pnrr_missione|^missione$", "programma_code"), (r"codice_indicatore|COD_INDICATORE", "indicatore_code"), (r"codice_miur|codice_ente_miur", "miur_code"), - (r"^username$", "sogei_code"), (r"^COD_ATECO|codice_ateco", "ateco_code"), - (r"codice_istat_regione|regione_codice_istat|codice_regione_istat", "region_code"), (r"nuts_parent_code", "nuts_code"), - (r"provincia_cm_codice_istat", "province_code"), + # Municipality: solo contesto comune — non "username" generico + ( + r"^codice_istat$|^cod_comune$|^codice_comune$|^comune_istat$" + r"|comune_codice_istat|codice_comune_", + "municipality_code", + ), ) # Colonne che sono label/descrizione di una chiave, non la chiave stessa. @@ -224,7 +232,8 @@ def suggest_semantic_type( """Suggerisce un semantic_type per una colonna civic-like. 1. Alias esatto (case-insensitive) dal vocabolario. - 2. Pattern civic forti (solo se il tipo esiste nel vocabolario). + 2. Pattern civic forti, ordinati specifico→generico (solo se il tipo + esiste nel vocabolario). Le colonne di descrizione/label (descr_*, descrizione_*, *_label) non vengono tipizzate come chiavi: sono testo legato alla chiave. @@ -252,8 +261,8 @@ def find_untyped_civic_keys( """Trova colonne civic senza semantic_type in un clean_catalog/registry. Non blocca: è un warning di qualità per il registry build. Le colonne - senza tipo che matchano alias/pattern civic meritano un tipo nel - dataset.yml o un alias nel vocabolario. + senza tipo che matchano alias/pattern civic meritano un alias in + ``semantic_types.yaml`` (copertura centralizzata nel toolkit). """ alias_map = load_semantic_types(semantic_types_path) valid_types = load_valid_types(semantic_types_path) diff --git a/toolkit/registry/semantic_types.yaml b/toolkit/registry/semantic_types.yaml index 79b6ea0..e87b68f 100644 --- a/toolkit/registry/semantic_types.yaml +++ b/toolkit/registry/semantic_types.yaml @@ -94,7 +94,7 @@ types: aliases: [regione, codice_regione, cod_regione, cod_reg, regione_codice, istat_regione, regione_istat_cod, regione_beneficiario, amministrazione_regione_sede, codice_istat_regione, - regione_codice_istat, codice_regione_istat, codice_regione_istat] + regione_codice_istat, codice_regione_istat] hub: hub_slug: comuni_master hub_column: regione From 927ff456c4d187f6c3ed556d02af76ab933a75d9 Mon Sep 17 00:00:00 2001 From: Zio Gabber <78922322+Gabrymi93@users.noreply.github.com> Date: Sun, 4 Oct 2026 18:50:57 +0100 Subject: [PATCH 3/3] =?UTF-8?q?refactor(registry):=20PR=20ridotta=20ad=20a?= =?UTF-8?q?lias=20semantic=5Ftypes=20=E2=80=94=20no=20motore=20suggeriment?= =?UTF-8?q?o?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Taglio per rapporto LOC/beneficio (review utente): - rimossi pattern/suggest/find_untyped_civic_keys da schema_reader - rimossi WARN CLI da cmd_registry - rimossi test del motore e del path CLI - resta solo semantic_types.yaml con alias estesi sui nomi reali del Lab (missione, CF OC/MEF/ANAC, catastali, ATECO, MIUR, provincia_cm, PIVA, ecc.) Al rebuild con parquet locale, toolkit assegna i tipi dagli alias. --- tests/test_cli_registry.py | 91 -------------- tests/test_semantic_civic_keys.py | 170 --------------------------- toolkit/cli/cmd_registry.py | 30 ----- toolkit/registry/schema_reader.py | 104 ---------------- toolkit/registry/semantic_types.yaml | 1 + 5 files changed, 1 insertion(+), 395 deletions(-) delete mode 100644 tests/test_semantic_civic_keys.py diff --git a/tests/test_cli_registry.py b/tests/test_cli_registry.py index 5862092..7861f49 100644 --- a/tests/test_cli_registry.py +++ b/tests/test_cli_registry.py @@ -173,94 +173,3 @@ def test_only_entities_fails_without_existing_registry(tmp_path: Path) -> None: ) assert result.exit_code != 0 assert "registry.json non esistente" in result.stderr - - -# --------------------------------------------------------------------------- -# WARN colonne civic senza semantic_type (PR #496) -# --------------------------------------------------------------------------- - - -@pytest.mark.pure_unit -def test_warn_untyped_civic_keys_unit_non_blocking(capsys) -> None: - """_warn_untyped_civic_keys: stampa, ritorna findings, non alza eccezioni.""" - from toolkit.cli.cmd_registry import _warn_untyped_civic_keys - - catalog = { - "datasets": [ - { - "slug": "demo", - "columns": [ - {"name": "missione", "type": "varchar", "role": "dimension"}, - { - "name": "cup", - "type": "varchar", - "role": "dimension", - "semantic_type": "cup_code", - }, - ], - } - ] - } - findings = _warn_untyped_civic_keys(catalog) - assert len(findings) == 1 - assert findings[0]["column"] == "missione" - captured = capsys.readouterr() - assert "colonne civic senza semantic_type" in captured.err - assert "semantic_types.yaml" in captured.err - assert "dataset.yml" not in captured.err # copy allineato all'architettura - - -@pytest.mark.pure_unit -def test_warn_untyped_civic_keys_empty_catalog(capsys) -> None: - from toolkit.cli.cmd_registry import _warn_untyped_civic_keys - - assert _warn_untyped_civic_keys({"datasets": []}) == [] - assert "colonne civic" not in capsys.readouterr().err - - -@pytest.mark.pure_unit -def test_registry_build_warns_untyped_civic_column_non_blocking(tmp_path: Path) -> None: - """Build con colonna civic non tipizzata → exit 0 + WARN (non blocca).""" - from tests.helpers import write_parquet - - ds_dir = tmp_path / "datasets" / "civic-demo" - ds_dir.mkdir(parents=True) - (ds_dir / "dataset.yml").write_text( - """ -root: "../../out" -schema_version: 1 -dataset: - name: "civic_demo" - source_id: "src1" - tags: [test] - category: demo - years: [2024] -registry: - description: "demo civic untyped" -""".strip() - + "\n", - encoding="utf-8", - ) - (ds_dir / "sql").mkdir() - (ds_dir / "sql" / "clean.sql").write_text("SELECT 1", encoding="utf-8") - - out = tmp_path / "out" / "data" / "clean" / "civic_demo" / "2024" - out.mkdir(parents=True) - write_parquet( - out / "civic_demo_2024_clean.parquet", - # nome civic via pattern (non alias): deve restare untyped e generare WARN - "CREATE TABLE t (codice_fiscale_demo VARCHAR, val DOUBLE); " - "INSERT INTO t VALUES ('ABC', 1.0)", - ) - - result = runner.invoke( - app, - ["registry", "build", "--repo", str(tmp_path), "--out", str(tmp_path / "reg")], - obj={}, - ) - # Non-blocking: exit 0 anche se ci sono WARN - assert result.exit_code == 0, result.stdout + result.stderr - combined = result.stdout + result.stderr - assert "colonne civic senza semantic_type" in combined - assert "codice_fiscale_demo" in combined - assert "fiscal_code" in combined diff --git a/tests/test_semantic_civic_keys.py b/tests/test_semantic_civic_keys.py deleted file mode 100644 index 6958315..0000000 --- a/tests/test_semantic_civic_keys.py +++ /dev/null @@ -1,170 +0,0 @@ -"""Test del suggerimento/censimento colonne civic senza semantic_type. - -Marker: pure_unit (logica pura sul vocabolario e sul catalogo in memoria). -""" - -from __future__ import annotations - -import pytest - -from toolkit.registry.schema_reader import ( - find_untyped_civic_keys, - load_semantic_types, - load_valid_types, - suggest_semantic_type, -) - - -class TestSuggestSemanticType: - @pytest.fixture(autouse=True) - def _vocab(self) -> None: - self.alias_map = load_semantic_types() - self.valid_types = load_valid_types() - - @pytest.mark.pure_unit - def test_alias_missione_is_programma(self) -> None: - assert ( - suggest_semantic_type("missione", self.alias_map, self.valid_types) == "programma_code" - ) - - @pytest.mark.pure_unit - def test_alias_codice_missione_bdap(self) -> None: - assert ( - suggest_semantic_type("codice_missione", self.alias_map, self.valid_types) - == "programma_code" - ) - - @pytest.mark.pure_unit - def test_alias_cf_variants(self) -> None: - assert ( - suggest_semantic_type("soggetto_ricevente_cf", self.alias_map, self.valid_types) - == "fiscal_code" - ) - assert ( - suggest_semantic_type("OC_CODICE_FISCALE_SOGG", self.alias_map, self.valid_types) - == "fiscal_code" - ) - - @pytest.mark.pure_unit - def test_alias_catastale_comune(self) -> None: - assert ( - suggest_semantic_type("codice_catastale_comune", self.alias_map, self.valid_types) - == "cadastral_code" - ) - - @pytest.mark.pure_unit - def test_pattern_municipality(self) -> None: - # pattern: non nel vocabolario ma civic-like - assert ( - suggest_semantic_type("codice_comune_sogg_titolare", self.alias_map, self.valid_types) - == "municipality_code" - ) - - @pytest.mark.pure_unit - def test_unknown_column_returns_none(self) -> None: - assert suggest_semantic_type("descrizione_libera", self.alias_map, self.valid_types) is None - assert suggest_semantic_type("stato_cup", self.alias_map, self.valid_types) is None - - @pytest.mark.pure_unit - def test_description_columns_not_suggested_as_keys(self) -> None: - # descrizioni di chiavi: non sono join key - assert suggest_semantic_type("descr_codice_ateco", self.alias_map, self.valid_types) is None - assert ( - suggest_semantic_type("descrizione_submisura", self.alias_map, self.valid_types) is None - ) - assert ( - suggest_semantic_type("desc_codice_catastale", self.alias_map, self.valid_types) is None - ) - - @pytest.mark.pure_unit - def test_specific_patterns_before_generic_municipality(self) -> None: - # Review PR #496: il generico codice_istat non deve catturare ipa/regione - assert ( - suggest_semantic_type("codice_istat_ipa", self.alias_map, self.valid_types) - == "ipa_code" - ) - assert ( - suggest_semantic_type("codice_istat_regione", self.alias_map, self.valid_types) - == "region_code" - ) - assert ( - suggest_semantic_type("codice_istat", self.alias_map, self.valid_types) - == "municipality_code" - ) - assert ( - suggest_semantic_type("comune_codice_istat", self.alias_map, self.valid_types) - == "municipality_code" - ) - assert ( - suggest_semantic_type("codice_comune_amministrazione", self.alias_map, self.valid_types) - == "municipality_code" - ) - # ipa_codice_istat: se non in alias, non deve diventare municipality - sug = suggest_semantic_type("ipa_codice_istat", self.alias_map, self.valid_types) - assert sug != "municipality_code" - - @pytest.mark.pure_unit - def test_no_sogei_pattern_for_generic_username(self) -> None: - # Review: ^username$ è troppo aggressivo — nessun pattern sogei_code - from toolkit.registry.schema_reader import _CIVIC_KEY_PATTERNS - - assert not any(s == "sogei_code" for _, s in _CIVIC_KEY_PATTERNS) - - @pytest.mark.pure_unit - def test_provincia_cm_is_province_not_municipality(self) -> None: - assert ( - suggest_semantic_type("provincia_cm_codice_istat", self.alias_map, self.valid_types) - == "province_code" - ) - - @pytest.mark.pure_unit - def test_piva_beneficiario_is_fiscal(self) -> None: - assert ( - suggest_semantic_type("beneficiario_partita_iva", self.alias_map, self.valid_types) - == "fiscal_code" - ) - - @pytest.mark.pure_unit - def test_empty_name_returns_none(self) -> None: - assert suggest_semantic_type("", self.alias_map, self.valid_types) is None - - -class TestFindUntypedCivicKeys: - @pytest.mark.pure_unit - def test_finds_untyped_and_skips_typed(self) -> None: - catalog = { - "datasets": [ - { - "slug": "demo", - "columns": [ - {"name": "missione", "type": "varchar", "role": "dimension"}, - { - "name": "cup", - "type": "varchar", - "role": "dimension", - "semantic_type": "cup_code", - }, - {"name": "valore", "type": "double", "role": "metric"}, - ], - } - ] - } - findings = find_untyped_civic_keys(catalog) - assert len(findings) == 1 - assert findings[0]["slug"] == "demo" - assert findings[0]["column"] == "missione" - assert findings[0]["suggested"] == "programma_code" - - @pytest.mark.pure_unit - def test_empty_catalog(self) -> None: - assert find_untyped_civic_keys({"datasets": []}) == [] - assert find_untyped_civic_keys({}) == [] - - @pytest.mark.pure_unit - def test_missing_vocab_returns_empty(self, tmp_path) -> None: - catalog = { - "datasets": [ - {"slug": "x", "columns": [{"name": "cf", "type": "varchar", "role": "dimension"}]} - ] - } - assert find_untyped_civic_keys(catalog, tmp_path / "nope.yaml") == [] diff --git a/toolkit/cli/cmd_registry.py b/toolkit/cli/cmd_registry.py index a3edffb..84b3ca5 100644 --- a/toolkit/cli/cmd_registry.py +++ b/toolkit/cli/cmd_registry.py @@ -135,33 +135,6 @@ def _setup_build( return layout, contract, existing_path -def _warn_untyped_civic_keys(catalog: dict[str, Any], limit: int = 20) -> list[dict[str, Any]]: - """Warning non-blocking: colonne civic senza semantic_type nel catalogo. - - Copertura centralizzata: il fix è un alias in ``semantic_types.yaml``, - non la dichiarazione sparsa nei ``dataset.yml`` dei repo. - """ - from toolkit.registry.schema_reader import find_untyped_civic_keys - - findings = find_untyped_civic_keys(catalog) - if not findings: - return [] - typer.echo( - f"WARN: {len(findings)} colonne civic senza semantic_type " - "(aggiungi un alias in toolkit semantic_types.yaml — copertura centralizzata)", - err=True, - ) - for item in findings[:limit]: - typer.echo( - f" WARN: {item['slug']}.{item['column']} → suggerito {item['suggested']} " - f"(via {item['via']})", - err=True, - ) - if len(findings) > limit: - typer.echo(f" WARN: ... e altre {len(findings) - limit} colonne", err=True) - return findings - - def registry_build( repo: str = typer.Option(None, "--repo", help="Root del repo (default: CWD)"), prefix: str = typer.Option("", "--prefix", help="Prefisso GCS (es. 'eurostat')"), @@ -213,7 +186,6 @@ def registry_build( f"entities aggiornate: {len(existing['entities']['entities'])} entità, " f"{len(existing['entities']['bridges'])} bridge" ) - _warn_untyped_civic_keys(catalog) if not write: typer.echo("Dry-run: usa --write per scrivere il file.") @@ -266,8 +238,6 @@ def registry_build( f"marts {len(registry['marts'])}, " f"signals {len(registry['signals'])} (repo: {layout.source_repo})" ) - _warn_untyped_civic_keys({"datasets": registry.get("datasets", [])}) - if not write: typer.echo("Dry-run: usa --write per scrivere il file.") return diff --git a/toolkit/registry/schema_reader.py b/toolkit/registry/schema_reader.py index fe72ce3..d9a40bf 100644 --- a/toolkit/registry/schema_reader.py +++ b/toolkit/registry/schema_reader.py @@ -10,7 +10,6 @@ from __future__ import annotations -import re from pathlib import Path from typing import Any @@ -186,106 +185,3 @@ def latest_clean_columns( if parquet is not None: return parquet_columns(parquet, alias_map), year return None, None - - -# Pattern civic "forti" per colonne che spesso non hanno alias espliciti. -# Ordinati dal più specifico al più generico: regione/ipa/provincia prima -# di municipality, che resta legato al contesto "comune" (mai un generico -# "codice_istat" che catturerebbe ipa/regione/provincia). -_CIVIC_KEY_PATTERNS: tuple[tuple[str, str], ...] = ( - # Specifici territoriali (PRIMA del generic comune) - (r"codice_istat_regione|regione_codice_istat|codice_regione_istat", "region_code"), - (r"codice_ipa|codice_ente_ipa|codice_istat_ipa", "ipa_code"), - (r"provincia_cm_codice|codice_provincia_cm", "province_code"), - (r"codice_catastale|codi_catastale|cod_catastale", "cadastral_code"), - # Chiavi denaro/progetto - (r"(^|_)cf$|_cf$|^cf_|codice_fiscale|partita_iva|^piva", "fiscal_code"), - (r"^cup$|^codice_cup$|codice_cup_", "cup_code"), - (r"^cig$|^codice_cig$|codice_cig_", "cig_code"), - (r"codice_ente_siope|codice_ente_bdap", "siope_code"), - (r"codice_missione|pnrr_missione|^missione$", "programma_code"), - (r"codice_indicatore|COD_INDICATORE", "indicatore_code"), - (r"codice_miur|codice_ente_miur", "miur_code"), - (r"^COD_ATECO|codice_ateco", "ateco_code"), - (r"nuts_parent_code", "nuts_code"), - # Municipality: solo contesto comune — non "username" generico - ( - r"^codice_istat$|^cod_comune$|^codice_comune$|^comune_istat$" - r"|comune_codice_istat|codice_comune_", - "municipality_code", - ), -) - -# Colonne che sono label/descrizione di una chiave, non la chiave stessa. -# Non suggerire semantic_type civico: il join forte resta sul codice. -_DESC_LABEL_RE = re.compile( - r"(^descr_|^desc_|_descrizione$|^descrizione_|_label$|^label_)", - re.IGNORECASE, -) - - -def suggest_semantic_type( - col_name: str, - alias_map: dict[str, str], - valid_types: set[str], -) -> str | None: - """Suggerisce un semantic_type per una colonna civic-like. - - 1. Alias esatto (case-insensitive) dal vocabolario. - 2. Pattern civic forti, ordinati specifico→generico (solo se il tipo - esiste nel vocabolario). - - Le colonne di descrizione/label (descr_*, descrizione_*, *_label) non - vengono tipizzate come chiavi: sono testo legato alla chiave. - - Returns: - Tipo suggerito o None se non riconosciuto. - """ - if not col_name: - return None - if _DESC_LABEL_RE.search(col_name): - return None - hit = alias_map.get(col_name.lower()) - if hit and hit in valid_types: - return hit - for pattern, suggested in _CIVIC_KEY_PATTERNS: - if suggested in valid_types and re.search(pattern, col_name, re.IGNORECASE): - return suggested - return None - - -def find_untyped_civic_keys( - catalog: dict[str, Any], - semantic_types_path: Path | None = None, -) -> list[dict[str, Any]]: - """Trova colonne civic senza semantic_type in un clean_catalog/registry. - - Non blocca: è un warning di qualità per il registry build. Le colonne - senza tipo che matchano alias/pattern civic meritano un alias in - ``semantic_types.yaml`` (copertura centralizzata nel toolkit). - """ - alias_map = load_semantic_types(semantic_types_path) - valid_types = load_valid_types(semantic_types_path) - if not alias_map and not valid_types: - return [] - - findings: list[dict[str, Any]] = [] - for ds in catalog.get("datasets") or []: - slug = ds.get("slug") or "" - for col in ds.get("columns") or []: - name = col.get("name") or "" - if not name or col.get("semantic_type"): - continue - suggested = suggest_semantic_type(name, alias_map, valid_types) - if not suggested: - continue - via = "alias" if alias_map.get(name.lower()) == suggested else "pattern" - findings.append( - { - "slug": slug, - "column": name, - "suggested": suggested, - "via": via, - } - ) - return findings diff --git a/toolkit/registry/semantic_types.yaml b/toolkit/registry/semantic_types.yaml index e87b68f..0b0896d 100644 --- a/toolkit/registry/semantic_types.yaml +++ b/toolkit/registry/semantic_types.yaml @@ -88,6 +88,7 @@ types: expr: "UPPER(TRIM({col}))" weight: 5 note: "provincia_cm_codice_istat (SAI/CAS) e' codice provincia, non comune a 6 cifre." + region_code: description: "Codice o nome della regione" entity: Regione