Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
4 changes: 2 additions & 2 deletions .github/workflows/ipa-registry.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: Anagrafica IPA

# Ogni mese scarica l'anagrafica degli enti dall'Indice PA, la riduce a codice,
# nome e acronimo, la verifica e la pubblica come asset della release fissa
# nome, acronimo e categoria (#44), la verifica e la pubblica come asset della release fissa
# `data-ipa` (senza versione: interessa una copia stabile e verificata, non le
# variazioni). Se la verifica fallisce, l'asset precedente resta com'è (#38).
# Le action sono fissate al commit: il job può scrivere le release.
Expand Down Expand Up @@ -39,7 +39,7 @@ jobs:
env:
GH_TOKEN: ${{ github.token }}
run: |
notes="Anagrafica degli enti dell'Indice PA (codice_ipa, denominazione, acronimo), aggiornata il $(date -u +%Y-%m-%d): $(cat build.log). Fonte: open data IPA, https://www.indicepa.gov.it/ipa-dati/dataset/enti. File stabile: https://github.com/${GITHUB_REPOSITORY}/releases/download/${TAG}/ipa-enti.csv"
notes="Anagrafica degli enti dell'Indice PA (codice_ipa, denominazione, acronimo, categoria, nome_categoria), aggiornata il $(date -u +%Y-%m-%d): $(cat build.log). Fonte: open data IPA, https://www.indicepa.gov.it/ipa-dati/dataset/enti e https://www.indicepa.gov.it/ipa-dati/dataset/categorie-enti. File stabile: https://github.com/${GITHUB_REPOSITORY}/releases/download/${TAG}/ipa-enti.csv"
if ! gh release view "$TAG" > /dev/null 2>&1; then
gh release create "$TAG" --title "Anagrafica IPA" --notes "$notes" --prerelease --latest=false
fi
Expand Down
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,14 @@ Tutte le modifiche rilevanti di questo progetto sono documentate qui.

Il formato segue [Keep a Changelog](https://keepachangelog.com/it/1.0.0/); il progetto segue [Semantic Versioning](https://semver.org/lang/it/).

## [Unreleased]

### Added

- **Enti senza schede nel RNDT** ([#44](https://github.com/ondata/openrndt/issues/44)): se `discover --what ipa --match` non trova nulla nel vocabolario, cerca nell'anagrafica completa dell'Indice PA (23.757 enti, ora nel pacchetto) e restituisce gli enti con `schede: 0`, con un avviso su stderr. `--match "comune di palermo"` dava un elenco vuoto, ora dà `c_g273`: il nome è giusto, l'ente non pubblica sul RNDT. Prima le categorie che pubblicano più schede (un Comune prima di una scuola). Lo stesso ripiego nei suggerimenti di `--org` su zero risultati. In libreria: `find_ipa_registry()`.
- **Categoria IPA dell'ente** nel vocabolario e nell'anagrafica: `categoria` e `nome_categoria` (`L6`, «Comuni e loro Consorzi e Associazioni»), dal dataset `categorie-enti` dell'Indice PA; colonna `categoria` nella tabella di `discover --what ipa`. Il file `ipa-enti.csv` della release `data-ipa` ha due colonne in più.
- Skill `rndt-explorer` 3.12.0: `discover --what ipa --match` per capire se un ente pubblica in proprio.

## [3.10.0] - 2026-10-10

### Changed
Expand Down
7 changes: 7 additions & 0 deletions LOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,12 @@
# LOG

## 2026-10-10 (issue #44, anagrafica IPA nel pacchetto e categoria)

- **Categoria**: nel file degli enti IPA i campi tipologici sono cinque (`Tipologia` 5 valori, `Codice_Categoria` 53, `Codice_natura` 40 ma solo su 10.280 enti e senza descrizioni, `Codice_ateco`, `Ente_in_liquidazione`); il nome della categoria sta in `categorie-enti` e la sua `Tipologia_categoria` coincide con `Tipologia` per tutti gli enti. Tenuti codice e nome della categoria.
- **Decisioni dell'utente**: anagrafica nel pacchetto in gzip (397 KB), rigenerata a ogni release con il vocabolario; ripiego anche per `--org`; `--match` non cerca nella categoria.
- **Ordine del ripiego**: prima per peso della categoria nel RNDT (somma delle schede del vocabolario), poi per nome. In ordine di codice `--org palermo` proponeva un ordine professionale, un ATO e un'accademia; ora Comune di Palermo, Città metropolitana, AMAT.
- Vocabolario rigenerato con l'anagrafica nuova (scansione completa, 271 s): conteggi identici, due campi in più per voce.

## 2026-10-10 (README, ente dal codice IPA)

- **README**: nella «Ricerca per ente» la via consigliata è il codice IPA (`discover --what ipa --match`, poi `--ipa`), `--org` la seconda; esempio Torino `c_l219` 269 schede, come `--org`. Aperta la #44: su `--match` senza risultati ripiego sull'anagrafica IPA (Palermo `c_g273`, 0 schede, oggi elenco vuoto).
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,7 +214,7 @@ openrndt search --ipa r_sardeg # 766 record, anche quelli con il

`--ipa` filtra sul codice IPA dell'ente titolare, che nel RNDT è il prefisso dell'id di ogni scheda. Non dipende da come è scritto il nome: la Città metropolitana di Torino ha due grafie, ARPA Veneto tre (per esteso, «ARPAV» e «…Protezione dell'Ambiente del Veneto»), e `--ipa cmto` e `--ipa arpa_ve` (245) le trovano tutte. Trova anche le schede senza ente responsabile (192 nel catalogo). Il prefisso nel catalogo non ha maiuscole uniformi (`PCM`, `R_SARDEG` e `r_sardeg`), e la CLI lo cerca senza distinguerle. Più codici si separano con la virgola.

`discover --what ipa` è un vocabolario offline dei codici presenti nel RNDT (179), con nome e acronimo dell'Indice PA; `--match` cerca nel codice, nel nome, nell'acronimo e nei nomi usati nelle schede (`--match arpae` → `arpa`). Un ente che non pubblica sul RNDT non c'è: `--match palermo` dà un elenco vuoto, perché il Comune di Palermo non ha schede.
`discover --what ipa` è un vocabolario offline dei codici presenti nel RNDT (179), con nome, acronimo e categoria dell'Indice PA (`L6`, «Comuni e loro Consorzi e Associazioni»); `--match` cerca nel codice, nel nome, nell'acronimo e nei nomi usati nelle schede (`--match arpae` → `arpa`). Se nel vocabolario non c'è nulla, cerca nell'anagrafica completa dell'Indice PA (23.757 enti, anche questa nel pacchetto) e restituisce gli enti con `schede: 0`: `--match "comune di palermo"` → `c_g273`, perché il Comune di Palermo è nell'Indice PA ma non pubblica sul RNDT. Il nome è giusto, quindi i dati vanno cercati presso un ente sovraordinato o per territorio. Lo stesso ente lo segnala `--org` su zero risultati.

Due limiti: `--ipa arpa_ve` non trova 11 schede ARPAV il cui id non ha il codice, e in pochi casi l'ente responsabile non è il titolare IPA (`m_d`, Ministero della Difesa, ha schede dell'Istituto Geografico Militare).

Expand Down
2 changes: 1 addition & 1 deletion knowledge/cli/discover.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Nessuna chiamata di rete: le codelist sono costanti Python in `codelists.py`. È
| `output_formats` | Formati del parametro `f` dell'API (`json`, `atom`, `csw`, `csv`, `kml`, …). |
| `search_params` | Parametri dell'endpoint search con note d'uso. |
| `lucene_fields` | Campi Elasticsearch interrogabili in `--q`, con suffissi (`_txt`, `_s`, `_dt`, `_i`, `_b`) e regole wildcard. |
| `ipa` | Vocabolario dei codici IPA presenti nel RNDT (non è in `all`): codice, se è nell'Indice PA, nome e acronimo IPA, nomi in `EnteResponsabile_s`, schede, grafie del prefisso. Con `--match <testo>` solo i codici il cui codice, nome, acronimo o ente lo contiene (`--match arpae` → `arpa`). In JSON `{"generato", "codici"}`. Lo genera `scripts/build_ipa_vocabulary.py` prima di ogni release. |
| `ipa` | Vocabolario dei codici IPA presenti nel RNDT (non è in `all`): codice, se è nell'Indice PA, nome e acronimo IPA, categoria IPA (`categoria`, `nome_categoria`: `L6`, «Comuni e loro Consorzi e Associazioni»), nomi in `EnteResponsabile_s`, schede, grafie del prefisso. Con `--match <testo>` solo i codici il cui codice, nome, acronimo o ente lo contiene (`--match arpae` → `arpa`); la categoria non entra nella ricerca. Se il vocabolario non trova nulla, `--match` cerca nell'anagrafica dell'Indice PA nel pacchetto (`data/ipa-enti.csv.gz`, 23.757 enti, 397 KB) e restituisce, con un avviso su stderr, gli enti senza schede nel RNDT: stessa forma, `schede: 0`, `enti` e `grafie` vuoti (#44). `--match "comune di palermo"` → `c_g273`; `palermo` → 41 enti, prima le categorie che pubblicano più schede nel RNDT (Comune e Città metropolitana prima di scuole e ordini). In JSON `{"generato", "codici"}`. Vocabolario e anagrafica li scrive `scripts/build_ipa_vocabulary.py` prima di ogni release. |

# Examples

Expand Down
2 changes: 1 addition & 1 deletion knowledge/cli/search.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ Interroga l'endpoint [/rest/metadata/search](/api/rndt-rest-api.md) del RNDT.
| `--bbox-crs` | CRS esplicito della bbox. Supportati: `EPSG:4326` (default implicito), `CRS:84`, `WGS84`. |
| `--org` | Ente responsabile (`EnteResponsabile_s`) che contiene il testo, senza distinzione di maiuscole: un'espressione regolare Lucene, `regione piemonte` → `/.*[rR][eE][gG]….*/`, lo stesso filtro del plugin openrndt-geolibre. Trova anche «Regione Piemonte - A1601B - …». Fino alla 3.6.0 cercava la frase su `apiso_OrganizationName_txt`, che contiene anche il contatto del metadato, e perdeva record (#23). Le sigle non funzionano, perché l'ente è scritto per esteso: `--org arpae` dà 0. In AND con gli altri filtri. Su zero risultati la CLI fa una query esplorativa sul campo del contatto, dove ci sono anche le sigle, e stampa i nomi di ente responsabile che somigliano a quello cercato, rilanciabili con `--org`. Se `--org` da solo trova zero ma il nome compare fra gli enti citati nel metadato (chi lo compila o lo distribuisce, come CSI Piemonte per la Regione, o una sigla), l'avviso dà il numero di schede e la query `--q 'apiso_OrganizationName_txt:"…"'` per trovarle. |
| `--org-exact` | Ente responsabile in forma esatta e case-sensitive su `EnteResponsabile_s`, lo stesso campo di `--org`. Alternativo a `--org`. |
| `--ipa` | Codice IPA dell'ente titolare, il prefisso dell'id (`apiso_Identifier_s`), senza distinzione di maiuscole: `r_sardeg` → `apiso_Identifier_s:/[rR]_[sS]…:.*/`, 766 schede, perché nel catalogo il prefisso è sia `R_SARDEG` sia `r_sardeg`. Più codici separati da virgola, in OR. In AND con gli altri filtri, `--org` compreso. Trova anche le schede senza `EnteResponsabile_s` (192 nel catalogo) e tiene insieme le grafie del nome. Un codice con caratteri diversi da lettere, cifre e `_` esce con codice 2. Non trova le 11 schede ARPAV il cui id non ha i due punti (`arpa_ve_TRIX2008`, `c0507261_RPercNicoMais`, …): `--ipa arpa_ve` ne trova 245, `--org` anche quelle. I codici: `discover --what ipa`. Su zero risultati di `--org` l'avviso propone i codici del vocabolario che contengono il testo (`--org arpae` → `--ipa arpa`). Nel link GeoLibre diventa `rndtIpa`, con gli stessi codici (plugin 0.3.11, #42). |
| `--ipa` | Codice IPA dell'ente titolare, il prefisso dell'id (`apiso_Identifier_s`), senza distinzione di maiuscole: `r_sardeg` → `apiso_Identifier_s:/[rR]_[sS]…:.*/`, 766 schede, perché nel catalogo il prefisso è sia `R_SARDEG` sia `r_sardeg`. Più codici separati da virgola, in OR. In AND con gli altri filtri, `--org` compreso. Trova anche le schede senza `EnteResponsabile_s` (192 nel catalogo) e tiene insieme le grafie del nome. Un codice con caratteri diversi da lettere, cifre e `_` esce con codice 2. Non trova le 11 schede ARPAV il cui id non ha i due punti (`arpa_ve_TRIX2008`, `c0507261_RPercNicoMais`, …): `--ipa arpa_ve` ne trova 245, `--org` anche quelle. I codici: `discover --what ipa`. Su zero risultati di `--org` l'avviso propone i codici del vocabolario che contengono il testo (`--org arpae` → `--ipa arpa`); se nel vocabolario non ce ne sono, gli enti dell'anagrafica IPA senza schede nel RNDT (`--org "comune di palermo"` → «nell'Indice PA ma senza schede nel RNDT: c_g273 (Comune di Palermo)», #44). Nel link GeoLibre diventa `rndtIpa`, con gli stessi codici (plugin 0.3.11, #42). |
| `--data-category`, `-c` | Una o più categorie ISO 19115 separate da virgola (es. `planningCadastre`). Tradotta internamente in `keywords_s:VAL` perché il parametro ufficiale `dataCategory` [non filtra](/api/known-issues.md). |
| `--time` | Intervallo temporale della risorsa `yyyy-mm-dd/yyyy-mm-dd`. |
| `--modified` | Intervallo di modifica del record nel catalogo (diverso da `--time`). |
Expand Down
56 changes: 40 additions & 16 deletions scripts/build_ipa_registry.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
"""Scarica l'anagrafica degli enti dall'Indice PA e la riduce a codice, nome e acronimo (#38).
"""Scarica l'anagrafica degli enti dall'Indice PA e la riduce a codice, nome, acronimo e categoria (#38, #44).

Il file esce ordinato per codice IPA in minuscolo, con le colonne
`codice_ipa,denominazione,acronimo`. Prima di scriverlo lo script lo verifica:
intestazione attesa, almeno `--min-rows` enti, codici non vuoti, distinti e fatti
solo di lettere, cifre e `_`. Se una verifica fallisce esce con errore e non
`codice_ipa,denominazione,acronimo,categoria,nome_categoria`. Il nome della
categoria viene dal dataset `categorie-enti`, che dà anche la tipologia
(Pubbliche Amministrazioni, Gestori di Pubblici Servizi, ...): non serve tenerla.
Prima di scriverlo lo script lo verifica: intestazione attesa, almeno
`--min-rows` enti, codici non vuoti, distinti e fatti solo di lettere, cifre e
`_`, ogni ente con una categoria che ha un nome. Se una verifica fallisce esce con errore e non
scrive nulla, così il workflow mensile non sostituisce la copia buona.

uv run python scripts/build_ipa_registry.py ipa-enti.csv
Expand All @@ -21,26 +24,44 @@
import httpx

IPA_CSV = "https://www.indicepa.gov.it/ipa-dati/datastore/dump/d09adf99-dc10-4349-8c53-27b1e5aa97b6?bom=True"
COLUMNS = ["codice_ipa", "denominazione", "acronimo"]
CATEGORIES_CSV = "https://www.indicepa.gov.it/ipa-dati/datastore/dump/84ebb2e7-0e61-427b-a1dd-ab8bb2a84f07?bom=True"
COLUMNS = ["codice_ipa", "denominazione", "acronimo", "categoria", "nome_categoria"]
CODE_RE = re.compile(r"^[a-z0-9_]+$")


def download() -> str:
def download(url: str) -> str:
with httpx.Client(timeout=120, follow_redirects=True, headers={"User-Agent": "openrndt-ipa-registry (+https://github.com/ondata/openrndt)"}) as client:
r = client.get(IPA_CSV)
r = client.get(url)
r.raise_for_status()
return r.content.decode("utf-8-sig")


def reduce(text: str) -> list[list[str]]:
def _reader(text: str, columns: set[str], what: str) -> csv.DictReader:
reader = csv.DictReader(io.StringIO(text))
missing = {"Codice_IPA", "Denominazione_ente", "Acronimo"} - set(reader.fieldnames or [])
missing = columns - set(reader.fieldnames or [])
if missing:
raise SystemExit(f"colonne mancanti nell'anagrafica IPA: {', '.join(sorted(missing))}")
rows = [
[(row["Codice_IPA"] or "").strip().lower(), (row["Denominazione_ente"] or "").strip(), (row["Acronimo"] or "").strip()]
for row in reader
]
raise SystemExit(f"colonne mancanti in {what}: {', '.join(sorted(missing))}")
return reader


def categories(text: str) -> dict[str, str]:
"""Codice categoria → nome, dal dataset `categorie-enti` (53 il 2026-10-10)."""
reader = _reader(text, {"Codice_categoria", "Nome_categoria"}, "categorie-enti")
return {(row["Codice_categoria"] or "").strip(): (row["Nome_categoria"] or "").strip() for row in reader}


def reduce(text: str, names: dict[str, str]) -> list[list[str]]:
reader = _reader(text, {"Codice_IPA", "Denominazione_ente", "Acronimo", "Codice_Categoria"}, "anagrafica IPA")
rows = []
for row in reader:
category = (row["Codice_Categoria"] or "").strip()
rows.append([
(row["Codice_IPA"] or "").strip().lower(),
(row["Denominazione_ente"] or "").strip(),
(row["Acronimo"] or "").strip(),
category,
names.get(category, ""),
])
return sorted(rows, key=lambda r: r[0])


Expand All @@ -56,6 +77,9 @@ def verify(rows: list[list[str]], min_rows: int) -> None:
problems.append(f"{len(codes) - len(set(codes))} codici ripetuti")
if any(not r[1] for r in rows):
problems.append("enti senza denominazione")
unnamed = sorted({r[3] for r in rows if not r[4]})
if unnamed:
problems.append(f"categorie senza nome in categorie-enti: {unnamed[:5]}")
if problems:
raise SystemExit("anagrafica IPA non valida: " + "; ".join(problems))

Expand All @@ -65,13 +89,13 @@ def main() -> None:
parser.add_argument("out", type=Path, help="CSV da scrivere")
parser.add_argument("--min-rows", type=int, default=20000, help="numero minimo di enti (23.757 il 2026-10-10)")
args = parser.parse_args()
rows = reduce(download())
rows = reduce(download(IPA_CSV), categories(download(CATEGORIES_CSV)))
verify(rows, args.min_rows)
with args.out.open("w", newline="", encoding="utf-8") as f:
w = csv.writer(f)
w.writerow(COLUMNS)
w.writerows(rows)
print(f"{args.out}: {len(rows)} enti, {sum(1 for r in rows if r[2])} con acronimo", file=sys.stderr)
print(f"{args.out}: {len(rows)} enti, {sum(1 for r in rows if r[2])} con acronimo, {len({r[3] for r in rows})} categorie", file=sys.stderr)


if __name__ == "__main__":
Expand Down
Loading
Loading