# normattiva-sdk > SDK Python non ufficiale per Normattiva, il portale della legge vigente SDK Python non ufficiale per Normattiva, il portale della legge vigente dello Stato italiano. Copre i quindici endpoint dell'API open data in versione sincrona e asincrona, legge il testo di un atto a qualunque data e ne esporta la storia completa. # Da dove cominciare # normattiva-sdk SDK Python non ufficiale per [Normattiva](https://www.normattiva.it), il portale della legge vigente dello Stato italiano. ```python from datetime import date from normattiva import Normattiva, codici with Normattiva() as normattiva: art2043 = normattiva.dettaglio(codici.CODICE_CIVILE.articolo(2043)) print(art2043.testo) divorzio = normattiva.dettaglio( "urn:nir:stato:legge:1970-12-01;898~art5", vigenza=date(2005, 1, 1) ) print(divorzio.finestra) # 1987-03-12 → 2023-02-27 ``` **Progetto indipendente e non ufficiale**, gratuito e in licenza [MIT](https://github.com/ireneburresi/normattiva-sdk/blob/main/LICENSE). Non è affiliato con l'[Istituto Poligrafico e Zecca dello Stato](https://www.ipzs.it), con [Normattiva](https://www.normattiva.it) né con la [Presidenza del Consiglio dei Ministri](https://www.governo.it), e non è approvato da loro. I dati arrivano da [dati.normattiva.it](https://dati.normattiva.it) in licenza [**CC BY 4.0**](https://creativecommons.org/licenses/by/4.0/deed.it), che obbliga a citarne la fonte. Il testo **non è autentico**: l'unico ufficiale è quello pubblicato sulla Gazzetta Ufficiale a mezzo stampa, che prevale in caso di discordanza. [Che cosa comporta](https://normattiva-sdk.ireneburresi.dev/progetto/licenza/index.md). ## Che cos'è Normattiva La banca dati che raccoglie il testo delle leggi dello Stato italiano, curata dall'Istituto Poligrafico e Zecca dello Stato. Di ogni atto conserva il testo vigente oggi e quello in vigore in ciascuna data del passato: ogni modifica apre una versione nuova senza cancellare la precedente, e la legge 241 del 1990 ne ha 61. Alla domanda «cosa dice questo articolo» va quindi sempre affiancato un «quando». Lo stesso corpus è pubblicato come open data su [dati.normattiva.it](https://dati.normattiva.it), con un'API HTTP gratuita. `normattiva-sdk` la interroga da Python, in versione sincrona e asincrona, e traduce le risposte in oggetti tipizzati. Se il diritto italiano non è il tuo mestiere, [come funziona la normativa italiana](https://normattiva-sdk.ireneburresi.dev/capire/la-normativa-italiana/index.md) spiega chi fa le leggi, che rango hanno, come cambiano nel tempo e come si scrive l'identificatore di ciascun tipo di atto. ## Da dove cominciare - **[Tutorial](https://normattiva-sdk.ireneburresi.dev/tutorial/index.md)** Una lezione da fare al terminale. Si parte dal `pip install` e si arriva a leggere un articolo, cercarlo per parole e percorrerne la storia. Comincia da qui se non hai mai usato la libreria. - **[Come fare](https://normattiva-sdk.ireneburresi.dev/come-fare/index.md)** Una guida per obiettivo: installare, identificare un atto, cercarlo, leggerne il testo a una data, esportarlo intero, lavorare in asincrono, usare la riga di comando. Vieni qui quando sai già che cosa vuoi ottenere. - **[Riferimento](https://normattiva-sdk.ireneburresi.dev/riferimento/index.md)** Classi, metodi, parametri, eccezioni, endpoint e comandi, con la firma esatta di ciascuno. Vieni qui quando ti serve un dettaglio preciso. - **[Capire](https://normattiva-sdk.ireneburresi.dev/capire/index.md)** Com'è fatto un atto, com'è fatto il servizio, che cosa fa la libreria quando il servizio risponde male, e perché è fatta così. Vieni qui quando vuoi il quadro d'insieme. ## Cosa si può chiedere | Cosa | Come | | ------------------------------------------------ | ----------------------------------- | | Il testo di un atto o di un articolo, a una data | dettaglio | | Tutte le versioni di un articolo | cronologia | | Ricerca a testo pieno e per coordinate | ricerca, ricerca_avanzata | | Tutte le pagine di una ricerca | ricerca_completa | | Gli atti modificati in un periodo | atti_aggiornati | | Export di atti interi, multivigente | start_export | | Archivi già confezionati | collections, download_collection | | I dizionari del servizio | denominazioni, classi_provvedimento | Le stesse capacità sono disponibili dal terminale, con il comando `normattiva`: ```bash normattiva testo codice-civile --articolo 2043 normattiva cerca procedimento amministrativo --anno 1990 --faccette normattiva esporta --denominazione LEGGE --anno 1990 --numero 241 --archivio 241.zip ``` ## Prima di metterla in produzione Il servizio ha comportamenti che danno un risultato plausibile e sbagliato senza sollevare nessun errore: un articolo troncato sembra un articolo corto. Le guide di [come fare](https://normattiva-sdk.ireneburresi.dev/come-fare/index.md) segnalano ciascun caso nel punto in cui può capitare. Il testo che ottieni non è autentico e in caso di discordanza prevale la Gazzetta Ufficiale; se lo ripubblichi, l'obbligo di attribuzione passa a te. Che cosa comporta, in pratica, sta in [licenza e attribuzione](https://normattiva-sdk.ireneburresi.dev/progetto/licenza/index.md). # Primi passi L'API di Normattiva non chiede chiavi né registrazione: servono solo Python e una connessione. ```bash pip install normattiva-sdk ``` ## Apriamo il client `Normattiva` è la classe da cui passa tutto: apre le connessioni verso l'API e ha un metodo per ciascuna cosa che si può chiedere. Il `with` la chiude quando il blocco finisce. ```python from normattiva import Normattiva with Normattiva() as normattiva: ... ``` Il codice che segue sta dentro quel blocco. ## Leggiamo un articolo Gli atti si indirizzano con un URN. Quello che segue si legge «articolo 1 della legge dello Stato del 7 agosto 1990, numero 241», cioè la legge sul procedimento amministrativo. ```python atto = normattiva.dettaglio("urn:nir:stato:legge:1990-08-07;241~art1") print(atto.titolo) print(atto.testo) ``` ```text LEGGE 7 agosto 1990, n. 241 Art. 1 (Principi generali dell'attività amministrativa) 1. L'attività amministrativa persegue i fini determinati dalla legge ed è retta da criteri di economicità, di efficacia, di imparzialità, di pubblicità e di trasparenza secondo le modalità previste dalla presente legge ... ``` Il primo `print` scrive il nome per esteso dell'atto, il secondo il testo dell'articolo. Notiamo che il testo comincia dal numero dell'articolo e dalla sua **rubrica**, il titoletto fra parentesi. ## Guardiamo che altro è arrivato La risposta porta molto più del testo. Chiediamole qualche altra cosa: ```python print(atto.commi[0]) print(atto.finestra) print(atto.gazzetta) print(atto.permalink) ``` ```text Comma(numero='1', testo="L'attività amministrativa persegue i fini ...") 2020-09-15 → oggi G.U. n. 192 del 1990-08-18 https://www.normattiva.it/uri-res/N2Ls?urn:nir:stato:legge:1990-08-07;241 ``` I **commi** sono i capoversi numerati dell'articolo, già separati uno per uno. La **gazzetta** dice dove l'atto è stato pubblicato, e il **permalink** è il link alla sua pagina su Normattiva: è quello da mettere in un documento, perché chi legge possa verificare sulla fonte. Guardiamo la **finestra**: comincia il 15 settembre 2020, non nel 1990. Dice da quando a quando vale il testo che abbiamo appena stampato, e ci sta dicendo che anche l'articolo 1 di questa legge è stato riscritto, l'ultima volta nel 2020. Se articolo, comma e rubrica non ti sono familiari, il vocabolario è spiegato in [come è fatto un atto](https://normattiva-sdk.ireneburresi.dev/capire/come-e-fatto-un-atto/index.md). Per la lezione basta quello che abbiamo appena visto. La finestra è il punto di partenza della prossima lezione: [il testo a una data](https://normattiva-sdk.ireneburresi.dev/tutorial/il-testo-a-una-data/index.md). # Come fare # Come fare Una pagina per obiettivo. Se è la prima volta, comincia invece dal [tutorial](https://normattiva-sdk.ireneburresi.dev/tutorial/index.md). | Per | Vai a | | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | mettere la libreria in un progetto | [Installare la libreria](https://normattiva-sdk.ireneburresi.dev/come-fare/installare/index.md) | | costruire o leggere l'identificatore di un atto | [Identificare un atto](https://normattiva-sdk.ireneburresi.dev/come-fare/identificare-un-atto/index.md) | | trovare un atto per parole o per coordinate | [Cercare un atto](https://normattiva-sdk.ireneburresi.dev/come-fare/cercare-un-atto/index.md) | | ottenere il testo com'era a una certa data | [Leggere il testo a una data](https://normattiva-sdk.ireneburresi.dev/come-fare/leggere-il-testo-a-una-data/index.md) | | scaricare un atto intero con tutte le versioni | [Esportare un atto intero](https://normattiva-sdk.ireneburresi.dev/come-fare/esportare-un-atto/index.md) | | fare le stesse cose senza bloccare il programma | [Lavorare in asincrono](https://normattiva-sdk.ireneburresi.dev/come-fare/lavorare-in-asincrono/index.md) | | interrogare Normattiva dal terminale, senza scrivere Python | [Usare la riga di comando](https://normattiva-sdk.ireneburresi.dev/come-fare/usare-la-riga-di-comando/index.md) | Per la firma esatta di un metodo, il [riferimento](https://normattiva-sdk.ireneburresi.dev/riferimento/index.md). Per il quadro d'insieme, [capire](https://normattiva-sdk.ireneburresi.dev/capire/index.md). # Cercare un atto Ci sono due ricerche, e rispondono a due domande diverse. - ricerca cerca **parole nel testo** degli atti. Serve quando sai di che cosa parla l'atto ma non come si chiama. - ricerca_avanzata cerca **coordinate**: tipo, anno, numero, date di emanazione e di pubblicazione. Serve quando l'atto lo sai già identificare, almeno in parte. Le due si combinano, perché `ricerca_avanzata` accetta anche un criterio `testo`. ## Cercare per parole ```python from normattiva import Normattiva with Normattiva() as normattiva: esito = normattiva.ricerca("silenzio assenso", per_pagina=3) print(esito.totale, "atti trovati") print("pagina", esito.pagina, "di", esito.pagine) for trovato in esito: print(trovato.citazione, "|", trovato.titolo[:50]) ``` ```text 65 atti trovati pagina 1 di 22 L. 20 aprile 2026, n. 50 | Conversione in legge, con modificazioni, del d D.L. 19 febbraio 2026, n. 19 | Ulteriori disposizioni urgenti per l'attu L. 2 dicembre 2025, n. 182 | Disposizioni per la semplificazione e la di ``` I numeri cambiano a ogni nuova pubblicazione: quelli qui sopra sono un esempio della forma, non un valore stabile. Il servizio combina le parole in **AND**: `"silenzio assenso"` trova gli atti che contengono entrambe le parole, ovunque siano nel testo. Non c'è modo di chiedere un OR né una frase esatta. ### Che cosa arriva indietro EsitoRicerca è **una pagina** di risultati, non tutti i risultati: | Attributo | Che cos'è | | ------------------ | ---------------------------------------------- | | `totale` | quanti atti ha trovato la ricerca in tutto | | `atti` | gli atti di questa pagina, come tupla | | `pagina`, `pagine` | il numero di questa pagina e quante ce ne sono | | `ultima_pagina` | `True` quando non c'è altro da chiedere | | `faccette` | i valori con cui restringere, vedi sotto | L'oggetto è iterabile, e itera sugli atti di questa pagina. `len(esito)` non esiste Non è definito apposta: `len` di una pagina di 20 risultati su 65 trovati non direbbe quale dei due numeri. Usa `len(esito.atti)` per questa pagina e `esito.totale` per la ricerca. Ogni elemento di `atti` è un AttoTrovato, che porta le coordinate dell'atto ma **non il testo**: ```python trovato = esito.atti[0] trovato.estremi.denominazione # 'LEGGE' trovato.estremi.data # datetime.date(2026, 4, 20) trovato.estremi.numero # '50' trovato.citazione # 'L. 20 aprile 2026, n. 50' trovato.titolo # 'Conversione in legge, con modificazioni, del ...' trovato.gazzetta # G.U. n. 91 del 2026-04-20 trovato.gazzetta.codice_redazionale # '26G00067' trovato.ha_urn # True trovato.urn # urn:nir:stato:legge:2026-04-20;50 ``` Il **codice redazionale** è l'identificativo che IPZS assegna al singolo documento pubblicato in Gazzetta. Non è leggibile e non è un URN, ma è l'unico identificatore che hanno gli atti per cui una forma URN non esiste: vedi [identificare un atto](https://normattiva-sdk.ireneburresi.dev/come-fare/identificare-un-atto/#dal-risultato-di-una-ricerca-allurn). ### Dal risultato al testo Il testo costa una seconda richiesta, e si chiede passando il risultato stesso a `dettaglio`, senza ricostruire nessun identificatore: ```python for trovato in normattiva.ricerca_completa("responsabilità civile", massimo=5): atto = normattiva.dettaglio(trovato) print(trovato.citazione, len(atto.testo), "caratteri") ``` `dettaglio` accetta l'`AttoTrovato` e sceglie da sé la strada: per URN dove la forma è verificata, per coordinate di Gazzetta dove non lo è. ### Restringere con le faccette Ogni risposta porta tre elenchi di valori con cui restringere la ricerca. Arrivano dentro la risposta della ricerca stessa, quindi leggerli non costa una richiesta in più. ```python esito = normattiva.ricerca("silenzio assenso") print(esito.faccette.per_tipo[:3]) print(esito.faccette.per_anno[:3]) ``` ```text (Faccetta(codice='PLE', conteggio=21, descrizione='LEGGE'), Faccetta(codice='PLL', conteggio=15, descrizione='DECRETO LEGISLATIVO'), Faccetta(codice='PDL', conteggio=14, descrizione='DECRETO-LEGGE')) (Faccetta(codice='2010', conteggio=6, descrizione='2010'), Faccetta(codice='2011', conteggio=5, descrizione='2011'), Faccetta(codice='2015', conteggio=4, descrizione='2015')) ``` Di ogni Faccetta: `codice` è il valore da passare come filtro, `descrizione` è quella da mostrare a chi legge, `conteggio` dice quanti atti restano scegliendo quella voce. I codici come `PLE` o `PLL` sono quelli interni del servizio; l'elenco completo lo restituisce `denominazioni()`. Le tre faccette si ripassano alla ricerca come parametri: ```python esito = normattiva.ricerca("silenzio assenso", tipo="PLE", anno=2010) ``` `anno` vuol dire due cose diverse In `ricerca`, `tipo`, `anno` ed `emettitore` sono **faccette**: restringono l'elenco che la ricerca ha già trovato. In `ricerca_avanzata`, `anno` è invece l'anno di emanazione dell'atto, cioè una sua coordinata. Portano lo stesso nome perché così li chiama il servizio. ## Cercare per coordinate ```python from datetime import date from normattiva import ClasseProvvedimento, Normattiva with Normattiva() as normattiva: esito = normattiva.ricerca_avanzata( denominazione="DECRETO-LEGGE", emanazione=(date(2020, 3, 1), date(2020, 6, 30)), classe=ClasseProvvedimento.AGGIORNATO, per_pagina=50, ) print(esito.totale) ``` La risposta ha la stessa forma di quella di `ricerca`, faccette comprese. I criteri accettati sono tipo, data a pezzi (`anno`, `mese`, `giorno`), numero, parole nel titolo o nel testo, vigenza a una data, classe redazionale e i due intervalli di date. L'elenco completo, con il tipo di ciascuno, sta in ricerca_avanzata. **`denominazione` vuole il nome esatto del dizionario**, cioè `"LEGGE"` o `"DECRETO LEGISLATIVO"`, non l'abbreviazione. I valori ammessi li elenca `denominazioni()`, ed è l'unico modo di conoscerli: non c'è una regola per ricavarli. Che differenza ci sia fra i tipi di atto lo spiega [come è fatto un atto](https://normattiva-sdk.ireneburresi.dev/capire/come-e-fatto-un-atto/#i-tipi-di-atto). **Gli intervalli sono coppie, e un estremo può mancare:** ```python emanazione = (date(2020, 1, 1), None) # dal 2020 in poi emanazione = (None, date(1950, 12, 31)) # fino al 1950 ``` **`classe` è la classificazione redazionale dell'atto**, non il suo stato giuridico: `SENZA_AGGIORNAMENTI` è un atto mai modificato, `AGGIORNATO` un atto modificato almeno una volta, `ABROGATO` un atto abrogato. Che cosa comporti l'abrogazione lo spiega [come è fatto un atto](https://normattiva-sdk.ireneburresi.dev/capire/come-e-fatto-un-atto/#la-vita-di-un-atto-nel-tempo). Senza nessun criterio la ricerca avanzata risponde con l'intero corpus, oltre duecentomila atti: è una richiesta ammessa, e la prima pagina costa quanto qualunque altra. ## Scorrere tutte le pagine `ricerca_completa` scorre le pagine da solo. È un iteratore **pigro**: chiede una pagina alla volta, e solo quando la precedente è esaurita. ```python for trovato in normattiva.ricerca_completa("divorzio"): print(trovato.citazione) ``` ```python async for trovato in normattiva.ricerca_completa("divorzio"): print(trovato.citazione) ``` Consumarne dieci risultati costa una richiesta sola, non tutte quelle che servirebbero ad arrivare in fondo. `massimo` ferma l'iterazione: ```python primi_dieci = list(normattiva.ricerca_completa("appalti", massimo=10)) ``` Qui `massimo` **limita** senza rifiutare: se gli atti sono di più, gli altri semplicemente non vengono prodotti. Nell'esportazione lo stesso concetto si comporta all'opposto, e il perché sta in [perché la libreria fa così](https://normattiva-sdk.ireneburresi.dev/capire/scelte/#limitare-o-rifiutare). Per sapere quanti sono prima di scorrerli basta una `ricerca` con una pagina minima: ```python quanti = normattiva.ricerca("appalti", per_pagina=1).totale if quanti < 500: atti = list(normattiva.ricerca_completa("appalti")) ``` `per_pagina` decide quante richieste servono Il predefinito è 50 in `ricerca_completa` e 20 in `ricerca`. Alzarlo riduce il numero di richieste a parità di risultati, e con l'autolimitazione a due richieste al secondo la differenza è misurabile: mille atti a 20 per pagina sono cinquanta richieste e venticinque secondi, a 100 per pagina sono dieci richieste e cinque secondi. ## Gli atti modificati in un periodo `atti_aggiornati` risponde a una domanda diversa dalle due ricerche: quali atti sono stati **modificati** fra due date. ```python from datetime import date for atto in normattiva.atti_aggiornati(date(2026, 1, 1), date(2026, 6, 30)): print(atto.citazione, atto.ultima_modifica, atto.atti_modificanti) ``` ```text D.L. 22 maggio 2026, n. 89 2026-06-27 ('26G00129',) D.L. 30 aprile 2026, n. 63 2026-06-27 ('26G00129',) D.L. 30 aprile 2026, n. 62 2026-06-27 ('26G00128',) ``` `atti_modificanti` contiene i codici redazionali di Gazzetta degli atti che hanno prodotto la modifica. Non sono URN e non sono titoli: per risalire al testo di quegli atti servirebbe anche la loro data di pubblicazione, che il servizio qui non manda. «Aggiornato» vuol dire modificato, non pubblicato Un atto pubblicato dentro la finestra e mai più toccato non compare in questo elenco. Le pubblicazioni si chiedono con `ricerca_avanzata(pubblicazione=(dal, al))`. Il servizio rifiuta le finestre più lunghe di dodici mesi. La libreria le spezza da sé, quindi un intervallo di dieci anni funziona e costa dieci richieste: ```python storia = list(normattiva.atti_aggiornati(date(2016, 1, 1), date(2026, 1, 1))) ``` Se `al` precede `dal`, la libreria solleva RuleViolationError con il codice `DATE_INVERTITE` prima di toccare la rete. ## I dizionari del servizio I valori che i criteri accettano non sono liberi: li elenca il servizio. ```python for voce in normattiva.denominazioni(): print(voce.codice, voce.descrizione) ``` ```text COS COSTITUZIONE DCT DECRETO PCG DECRETO DEL CAPO DEL GOVERNO 3NA DECRETO DEL CAPO DEL GOVERNO, PRIMO MINISTRO SEGRETARIO DI STATO ... ``` Sono trenta denominazioni, molte storiche. Gli altri due dizionari sono più corti: ```python normattiva.classi_provvedimento() # (Tipologica(codice='1', descrizione='atto normativo – senza aggiornamenti'), # Tipologica(codice='2', descrizione='atto normativo – aggiornato'), # Tipologica(codice='3', descrizione='atto normativo – abrogato')) normattiva.export_formats() # (Tipologica(codice='AKN', descrizione='Esporta AKN'), ...) ``` I tre dizionari cambiano di rado, quindi la libreria li tiene in memoria dopo la prima chiamata. Per forzare una rilettura, `reload=True`. # Esportare un atto intero `dettaglio` restituisce un articolo alla volta, a una data alla volta. Quando serve un atto **intero**, con tutti i suoi articoli e tutte le versioni che ha avuto, si usa l'esportazione. La legge 241 del 1990 ha 61 versioni. Ricostruirle con `cronologia`, articolo per articolo, costerebbe migliaia di richieste; un'esportazione le consegna tutte in un archivio ZIP che si salva su disco e si rilegge senza rete. ## Come funziona L'esportazione è l'unica parte dell'API che non risponde subito. Il servizio apre un lavoro, lo mette in coda e ci mette circa un minuto a completarlo: 1. **`start_export`** manda i criteri e riceve un **token**. Il lavoro è partito dalla parte del servizio. 1. **`wait`** interroga il servizio ogni quattro secondi finché l'archivio è pronto. 1. **`download`** scarica il file e lo legge in modelli. ```mermaid sequenceDiagram autonumber participant P as il tuo programma participant S as servizio P->>S: start_export(criteri) S-->>P: token loop wait(), ogni quattro secondi P->>S: stato dell'esportazione? S-->>P: PROCESSING, 36/300 atti end S-->>P: COMPLETED P->>S: download() S-->>P: archivio ZIP ``` I tre passi restano separati perché fra l'uno e l'altro c'è spazio: durante l'attesa il programma può fare altro, e il lavoro sopravvive al processo che lo ha avviato, così un'esportazione interrotta si riprende dal token invece di ricominciare. ```python from normattiva import Normattiva with Normattiva() as normattiva: esportazione = normattiva.start_export(anno=1990, numero=241) esportazione.wait() # circa un minuto corpus = esportazione.download() atto = corpus.atti[0] print(atto.estremi.citazione) print(len(atto.versioni), "versioni,", len(atto.aggiornamenti), "aggiornamenti") ``` ```text L. 7 agosto 1990, n. 241 61 versioni, 60 aggiornamenti ``` Le versioni sono sempre una più degli aggiornamenti: la prima è il testo originale, e ogni aggiornamento ne produce una nuova. ## Scegliere che cosa esportare I criteri sono gli stessi di [`ricerca_avanzata`](https://normattiva-sdk.ireneburresi.dev/come-fare/cercare-un-atto/#cercare-per-coordinate), quindi un'esportazione può prendere un atto solo o tutti quelli che una ricerca trova: ```python from datetime import date esportazione = normattiva.start_export( denominazione="DECRETO-LEGGE", emanazione=(date(2020, 3, 1), date(2020, 6, 30)), massimo_atti=60, ) ``` Due criteri esistono solo qui, e servono a togliere atti dal risultato: ```python esportazione = normattiva.start_export( testo="amministrativo", escludi_testo="trasparenza", # via gli atti che contengono questa parola escludi_titolo="regolamento", # via quelli il cui titolo la contiene ) ``` ### Quante versioni includere ```python from normattiva import ExportMode normattiva.start_export(anno=1990, numero=241, mode=ExportMode.MULTIVIGENTE) # predefinito normattiva.start_export(anno=1990, numero=241, mode=ExportMode.VIGENTE) normattiva.start_export(anno=1990, numero=241, mode=ExportMode.ORIGINALE) ``` `MULTIVIGENTE` include tutte le versioni ed è il predefinito. `VIGENTE` include solo il testo di oggi e `ORIGINALE` solo quello di prima pubblicazione: sono archivi molto più piccoli, utili quando la storia non serve. ### Il limite qui rifiuta `massimo_atti` conta gli atti **prima** di avviare l'esportazione e, se sono più del limite, non la avvia affatto: ```python from normattiva import TooManyResultsError try: normattiva.start_export(denominazione="LEGGE") except TooManyResultsError as errore: print(errore.totale, "atti, limite", errore.massimo) ``` ```text 32686 atti, limite 100 ``` Il predefinito è cento. Per alzarlo, o per togliere del tutto il conteggio: ```python normattiva.start_export(anno=2020, massimo_atti=500) normattiva.start_export(anno=2020, massimo_atti=None) # parte senza contare ``` È il contrario di `massimo` nella ricerca, che invece limita i risultati senza rifiutare la richiesta: il perché sta in [perché la libreria fa così](https://normattiva-sdk.ireneburresi.dev/capire/scelte/#limitare-o-rifiutare). Il conteggio non conosce le esclusioni Il conteggio preventivo passa dalla ricerca sincrona, che `escludi_testo` ed `escludi_titolo` non li prevede. Può quindi contare più atti di quanti ne arriveranno davvero: per un limite di sicurezza una stima per eccesso va bene. ## Attendere ```python stato = esportazione.wait() # scadenza predefinita: dieci minuti stato = esportazione.wait(timeout=120) ``` `wait` blocca il thread e interroga il servizio ogni quattro secondi. ```python import time while not esportazione.refresh().done: print(esportazione.progress) time.sleep(5) ``` `refresh` fa una domanda sola e restituisce lo stato, così il ritmo lo decidi tu. ```python esportazione = await normattiva.start_export(anno=1990, numero=241) await esportazione.wait() corpus = await esportazione.download() ``` `AsyncExport` ha gli stessi metodi, e l'attesa passa da `asyncio.sleep` invece di bloccare il ciclo di eventi. `progress` è un Progress e stampa `'36/300 atti'` quando il servizio manda il conteggio, `'12%'` quando manda solo la percentuale. Il conteggio è più informativo: una percentuale ferma non distingue un lavoro lento da un lavoro bloccato. Gli stati possibili, e quali di questi concludono l'attesa, stanno in ExportStatus. Il ritardo dichiarato vale una proroga sola Il servizio può rispondere `CONFIRMED_WITH_DELAY`, cioè «ci metterò più del previsto». La libreria concede una proroga pari alla scadenza, **una volta**: rinnovarla a ogni dichiarazione toglierebbe ogni limite all'attesa, e `wait(timeout=...)` non vorrebbe più dire niente. ## Riprendere da un token Il lavoro sta sul servizio, non nel processo che lo ha chiesto: ```python token = esportazione.token salva_da_qualche_parte(token) # in un altro processo, anche dopo un riavvio esportazione = normattiva.export_from_token(token) esportazione.wait() corpus = esportazione.download() ``` `export_from_token` interroga subito lo stato, quindi si sa immediatamente se il lavoro è ancora in corso o già pronto. ## Che cosa c'è dentro l'archivio `download` restituisce un Corpus, che contiene un AttoStorico per ogni atto esportato: ```python len(corpus) # quanti atti for atto in corpus: # AttoStorico ... ``` Un `AttoStorico` è l'atto con tutta la sua storia: ```python atto = corpus.atti[0] str(atto.urn) # 'urn:nir:stato:legge:1990-08-07;241' atto.estremi.citazione # 'L. 7 agosto 1990, n. 241' atto.gazzetta # G.U. n. 192 del 1990-08-18 atto.pubblicato_il # datetime.date(1990, 8, 18) atto.abrogato # False atto.versioni # tutte, dalla più vecchia alla più recente atto.aggiornamenti # le modifiche, come le descrive il servizio ``` `abrogato` segnala lo stato dell'atto, e il testo resta comunque disponibile; che cosa comporti l'abrogazione lo spiega [come è fatto un atto](https://normattiva-sdk.ireneburresi.dev/capire/come-e-fatto-un-atto/#la-vita-di-un-atto-nel-tempo). Ogni Aggiornamento descrive una modifica con le parole del servizio: ```python print(atto.aggiornamenti[0].data) print(atto.aggiornamenti[0].testo) ``` ```text 2019-10-05 ha disposto (con l'art. 4, comma 1) la modifica dell'art. 6, comma 1, lettera e). ``` ### La versione a una data ```python from datetime import date versione = atto.alla_data(date(2005, 1, 1)) versione.vigente_dal # datetime.date(2004, 4, 29) versione.originale # False ``` `alla_data` restituisce l'ultima versione entrata in vigore **prima** della data richiesta, che è quella che quel giorno era valida. Se la data precede la pubblicazione dell'atto solleva VersionNotFoundError. Le due versioni agli estremi hanno una scorciatoia: ```python atto.originale # com'è stato pubblicato atto.vigente # la più recente contenuta nell'archivio ``` La versione originale nell'archivio non porta una data di inizio: quella è la data di pubblicazione dell'atto, che sta su `atto.pubblicato_il`. Un atto mai modificato ha una sola versione, e vale da allora. ### L'articolato Ogni VersioneAtto contiene un albero di Partizione: libri, titoli, capi, articoli. Per scendere direttamente agli articoli c'è `articoli()`: ```python for articolo in atto.vigente.articoli(): print(articolo.numero, "|", articolo.rubrica) ``` ```text 1 | Principi generali dell'attivita' amministrativa 2 | Conclusione del procedimento 2 bis | Conseguenze per il ritardo dell'amministrazione nella conclusione del procedimento. 3 | Motivazione del provvedimento 3 bis | Uso della telematica. ``` Il `numero` è una stringa, non un intero: `2 bis` è un numero di articolo del tutto normale. La `rubrica` è il titolo dell'articolo. La rubrica manca quasi sempre nelle versioni vecchie Nella legge 241 la versione vigente ha la rubrica su 50 articoli su 51. Quella in vigore nel 2005 ne ha **zero** su 34: il numero c'è sempre, il titolo dell'articolo no. Un programma che indicizza per rubrica perde tutta la storia più vecchia senza segnalare niente. Gli **allegati** stanno in un ramo separato, `versione.annessi`, perché non fanno parte dell'articolato e contarli insieme darebbe conteggi sbagliati. È anche il ramo che contiene i codici: nell'export del codice civile, `articoli()` trova due articoli e non 3280. Nell'export gli accenti sono vocale più apostrofo Il testo dell'esportazione scrive `attivita'` dove il percorso interattivo scrive `attività`, come si vede nella rubrica dell'articolo 1 qui sopra: una ricerca sulla grafia corretta non troverebbe nulla. `normalize_accents` la rimette a posto: ```python from normattiva import normalize_accents normalize_accents("l'attivita' e' liberta'") # "l'attività è libertà" ``` ## Salvare e riaprire Un archivio scaricato si mette da parte e si rilegge senza toccare la rete: ```python from normattiva import Corpus corpus.save("241.zip") riaperto = Corpus.from_zip("241.zip") ``` Su un atto voluminoso conviene: lo si scarica una volta e lo si interroga quante volte serve, senza far ripartire un minuto di lavoro al servizio a ogni prova. La struttura interna dell'archivio, e che cosa succede se la convenzione dei nomi cambia, stanno nel [riferimento](https://normattiva-sdk.ireneburresi.dev/riferimento/esportazione/#il-formato-dellarchivio). ## Gli altri formati Il servizio produce anche AKN, XML, PDF, EPUB, RTF e HTML. La libreria legge in modelli **solo il JSON**; gli altri si scaricano come file: ```python from normattiva import Format esportazione = normattiva.start_export(anno=1990, numero=241, format=Format.AKN) esportazione.wait() esportazione.save("241-akn.zip") ``` Chiamare `download()` su un formato che la libreria non legge fallisce subito, invece di consegnare un archivio che si scoprirebbe illeggibile più tardi: ```python esportazione.download() # InvalidArgumentError: il format AKN non viene letto in modelli: # usare save() per scaricarlo come file ``` ## Gli archivi già pronti Alcune collezioni tematiche il servizio le tiene già confezionate, e non richiedono nessuna attesa: ```python for collezione in normattiva.collections(): print(collezione.name, collezione.total_atti, collezione.created_at) normattiva.save_collection("Leggi di delegazione europea", "delega.zip") ``` `download_collection` restituisce un archivio vuoto Finché il servizio si comporta così, quelle collezioni si prendono con `save_collection`, che scrive il file su disco. # Identificare un atto Per chiedere un atto a Normattiva serve il suo indirizzo, che è un **URN NIR**, lo schema con cui le norme italiane si citano fra loro: ```text urn:nir:stato:legge:1990-08-07;241~art1 ``` Si legge «articolo 1 della legge dello Stato del 7 agosto 1990, numero 241». L'elenco delle parti di cui è composto sta nel [riferimento](https://normattiva-sdk.ireneburresi.dev/riferimento/urn/#le-parti-di-un-urn). La forma è rigida e il servizio non aiuta a scoprirlo: un separatore fuori posto, una data sbagliata o un allegato mancante producono un `404`, che è la stessa risposta che si riceve per un atto inesistente. Per questo conviene far comporre l'URN alla libreria invece di scriverlo a mano. ## Comporlo con i costruttori Per i cinque tipi di atto più comuni ci sono costruttori che mettono i pezzi al posto giusto: ```python from datetime import date from normattiva import Urn print(Urn.legge(1990, 241)) print(Urn.legge(1990, 241, data=date(1990, 8, 7))) print(Urn.decreto_legge(2020, 18)) print(Urn.decreto_legislativo(2005, 82)) print(Urn.dpr(2001, 380, articolo="6bis")) print(Urn.regio_decreto(1942, 262)) ``` ```text urn:nir:stato:legge:1990;241 urn:nir:stato:legge:1990-08-07;241 urn:nir:stato:decreto.legge:2020;18 urn:nir:stato:decreto.legislativo:2005;82 urn:nir:stato:decreto.del.presidente.della.repubblica:2001;380~art6bis urn:nir:stato:regio.decreto:1942;262 ``` Numeri e articoli si passano come interi o come stringhe, indifferentemente: `Urn.legge(1990, 241)` e `Urn.legge(1990, "241")` producono lo stesso URN. ### La data serve o no? Entrambe le forme rispondono. Quella con la data è più precisa, e serve quando in uno stesso anno esistono due atti con lo stesso numero, cosa che succede più spesso di quanto sembri: senza data quell'URN corrisponde a due atti distinti e la libreria solleva `AmbiguityError` invece di sceglierne uno. ### Gli articoli con l'ordinale Quando una modifica inserisce un articolo nuovo fra il 2 e il 3, gli articoli successivi non vengono rinumerati: si aggiunge un **2-bis**, poi un 2-ter, e avanti con gli ordinali latini. Nell'URN si scrivono attaccati e senza trattino: ```python Urn.legge(1990, 241, articolo="5bis") # va bene Urn.legge(1990, 241, articolo="5BIS") # normalizzato in 5bis Urn.legge(1990, 241, articolo="5-bis") # rifiutato ``` ```text InvalidUrnError: URN non valido: '5-bis' (numero di articolo non riconosciuto) ``` Il trattino viene rifiutato in locale, prima della richiesta, perché il servizio non lo accetta e risponderebbe con lo stesso `404` indistinguibile di sempre. ## Leggere un URN che arriva da fuori `Urn.parse` accetta la forma testuale e la scompone: ```python from normattiva import Urn urn = Urn.parse("urn:nir:stato:legge:1990-08-07;241~art5") urn.denominazione # 'legge' urn.anno # 1990 urn.data # datetime.date(1990, 8, 7) urn.numero # '241' urn.articolo # '5' urn.allegato # None ``` Se la stringa non è un URN valido, l'errore arriva subito, senza toccare la rete: ```python from normattiva import InvalidUrnError try: Urn.parse("urn:nir:stato:legge:1990-02-30;241") except InvalidUrnError as errore: print(errore) print(errore.testo, "|", errore.motivo) ``` ```text URN non valido: '1990-02-30' (data inesistente) 1990-02-30 | data inesistente ``` `testo` è il pezzo che non va e `motivo` la ragione, quando la libreria sa qual è. Su una stringa che non somiglia affatto a un URN, `motivo` resta `None`. ## Modificarne un pezzo `Urn` è immutabile: i metodi che sembrano modificarlo restituiscono un URN nuovo, e quello di partenza resta com'era. ```python legge = Urn.legge(1990, 241) legge.con_articolo(19) # ~art19 legge.con_articolo(19).con_vigenza(date(2000, 1, 1)) # !vig=2000-01-01 legge.con_vigenza("originale") # @originale ``` `permalink` restituisce il link pubblico alla pagina di Normattiva, quello da mettere in un documento perché chi legge possa verificare sulla fonte: ```python Urn.legge(1990, 241, articolo=1).permalink # 'https://www.normattiva.it/uri-res/N2Ls?urn:nir:stato:legge:1990;241~art1' ``` ## Il comma si porta ma non si chiede I rimandi dentro il testo restituito dal servizio arrivano spesso con il comma attaccato, e `Urn` lo sa leggere e conservare. Il servizio però **rifiuta** un URN che gli arriva col comma: ```python citazione = Urn.parse("urn:nir:stato:legge:2007-12-24;244~art2-com428") citazione.comma # '428' citazione.senza_comma # urn:nir:stato:legge:2007-12-24;244~art2 ``` `dettaglio` toglie il comma da sé prima di fare la richiesta, quindi non è una cosa di cui doversi ricordare. `senza_comma` serve quando l'URN lo maneggi tu, per esempio per costruire un link o una chiave di cache. ## I codici Un articolo del codice civile non risponde sotto l'URN del regio decreto che lo ha approvato. Risponde sotto un suo **allegato**: ```python from normattiva import Normattiva, codici with Normattiva() as normattiva: art = normattiva.dettaglio(codici.CODICE_CIVILE.articolo(2043)) print(codici.CODICE_CIVILE.articolo(2043)) print(art.testo) ``` ```text urn:nir:stato:regio.decreto:1942-03-16;262:2~art2043 Art. 2043. (Risarcimento per fatto illecito). Qualunque fatto doloso o colposo, che cagiona ad altri un danno ingiusto, obbliga colui che ha commesso il fatto a risarcire il danno. ``` Il `:2` prima dell'articolo è l'allegato. Il codice civile è l'allegato 2 del R.D. 262/1942, il codice penale è l'allegato 1 del R.D. 1398/1930, il codice di procedura penale non ha allegato. Non c'è una regola da applicare, e dedurre l'allegato per analogia porta a un `404`. `codici` conosce l'allegato di dodici atti fra i più citati. L'elenco, con la citazione di ciascuno, sta nel [riferimento](https://normattiva-sdk.ireneburresi.dev/riferimento/codici/index.md); per scorrerlo da codice: ```python for atto in codici.tutti(): print(f"{atto.nome:45} {atto.urn}") ``` Se il codice che ti serve non è nell'elenco, cercalo con `ricerca` e usa l'URN che il servizio stesso restituisce, invece di comporlo per tentativi. ## Dal risultato di una ricerca all'URN Ogni AttoTrovato espone `urn`, ricavato dalle sue coordinate. Per certi tipi di atto, però, la libreria non sa comporlo: ```python for trovato in normattiva.ricerca_completa("bonifica", massimo=20): if trovato.ha_urn: print(trovato.urn) else: print(trovato.citazione, "(URN non componibile)") ``` Sono dodici denominazioni su trenta, quasi tutte storiche: «regolamento», «decreto del Duce», «regio decreto-legge». Per quelle `urn` solleva InvalidUrnError invece di comporre un identificatore che il servizio rifiuterebbe, e `ha_urn` permette di saperlo prima. Restano comunque leggibili: `dettaglio` accetta l'`AttoTrovato` e per quegli atti passa dalle coordinate di Gazzetta, che il servizio accetta altrettanto bene. ```python atto = normattiva.dettaglio(trovato) ``` ```mermaid flowchart TD A["dettaglio(trovato)"] --> B{"la denominazione ha
una forma URN verificata?"} B -- sì --> C["atto/dettaglio-atto-urn
conosce la vigenza"] B -- no --> D["atto/dettaglio-atto
coordinate di Gazzetta"] D --> E{"hai chiesto
una vigenza?"} E -- sì --> F["InvalidArgumentError"] E -- no --> G["il testo di oggi"] ``` La strada di Gazzetta non conosce le date: una `vigenza` chiesta per un atto raggiungibile solo così solleva InvalidArgumentError, perché ignorarla restituirebbe il testo di oggi facendolo passare per quello storico. ## Citare un atto `citazione` scrive l'atto nella forma usata dai giuristi: ```python from datetime import date from normattiva import EstremiAtto print(EstremiAtto("LEGGE", date(1990, 8, 7), "241").citazione) print(EstremiAtto("REGIO DECRETO-LEGGE", date(1935, 1, 13), "1").citazione) ``` ```text L. 7 agosto 1990, n. 241 R.D.L. 13 gennaio 1935, n. 1 ``` Le abbreviazioni conosciute sono undici e le forme URN diciotto, e i due insiemi non coincidono: il regio decreto-legge si abbrevia ma non si indirizza, mentre otto tipi si indirizzano senza avere un'abbreviazione. Un tipo senza abbreviazione si cita per esteso. La tabella completa sta in [come è fatto un atto](https://normattiva-sdk.ireneburresi.dev/capire/come-e-fatto-un-atto/#le-abbreviazioni). Nella pratica si scrive poi «art. 2, comma 1, l. 241/1990». La libreria si ferma alla citazione dell'atto, l'unica parte per cui esiste una convenzione davvero condivisa. # Installare la libreria Il pacchetto si chiama `normattiva-sdk`, il modulo da importare si chiama `normattiva`. ```bash pip install normattiva-sdk ``` ```bash uv add normattiva-sdk ``` ```bash poetry add normattiva-sdk ``` ## Che cosa serve Python da 3.10 a 3.14, e nient'altro da configurare: l'API open data di Normattiva risponde senza chiave, senza token e senza registrazione. L'unica dipendenza a runtime è [httpx](https://www.python-httpx.org/) da 0.28 in su. Il pacchetto porta `py.typed`, quindi mypy, pyright e ty leggono i tipi senza stub. L'installazione porta anche il comando `normattiva`, descritto in [usare la riga di comando](https://normattiva-sdk.ireneburresi.dev/come-fare/usare-la-riga-di-comando/index.md). ## Verificare che funzioni ```python from normattiva import Normattiva with Normattiva() as normattiva: atto = normattiva.dettaglio("urn:nir:stato:legge:1990-08-07;241~art1") print(atto.testo) ``` Se stampa il testo dell'articolo 1 della legge sul procedimento amministrativo, l'installazione è a posto. Il passo successivo è il [tutorial](https://normattiva-sdk.ireneburresi.dev/tutorial/index.md). Il client si riusa `Normattiva` tiene aperto un pool di connessioni e si autolimita a due richieste al secondo. Costruiscine uno per processo e passalo alle funzioni che ne hanno bisogno: l'autolimitazione conta le richieste di un client, quindi con un client per chiamata ogni richiesta parte senza attendere le altre. ## Lavorare sulla libreria stessa Per clonare il repository, far girare le prove e costruire la documentazione, vedi [sviluppo](https://normattiva-sdk.ireneburresi.dev/progetto/sviluppo/index.md). # Lavorare in asincrono `AsyncNormattiva` ha gli stessi metodi di `Normattiva`, con le stesse firme e lo stesso comportamento: cambia solo che vanno attesi con `await`. ```python import asyncio from normattiva import AsyncNormattiva async def main() -> None: async with AsyncNormattiva() as normattiva: atto = await normattiva.dettaglio("urn:nir:stato:legge:1990-08-07;241~art1") print(atto.testo) asyncio.run(main()) ``` ## Gli iteratori diventano asincroni ```python async for trovato in normattiva.ricerca_completa("appalti", massimo=100): print(trovato.citazione) async for versione in normattiva.cronologia(urn): print(versione.finestra) async for atto in normattiva.atti_aggiornati(dal, al): print(atto.citazione) ``` Restano pigri come gli equivalenti sincroni: una pagina alla volta, e solo quando serve. ## L'esportazione ```python esportazione = await normattiva.start_export(anno=1990, numero=241) await esportazione.wait() corpus = await esportazione.download() ``` `AsyncExport` ha gli stessi metodi e le stesse proprietà di `Export`, e `wait` lascia libero il ciclo di eventi: l'attesa fra un controllo e l'altro passa da `asyncio.sleep`. ## Concorrenza Il limitatore asincrono usa un `asyncio.Lock`, quindi più corutine che condividono lo stesso client si mettono in fila da sole: ```python async with AsyncNormattiva() as normattiva: atti = await asyncio.gather(*(normattiva.dettaglio(urn) for urn in urns)) ``` Le richieste partono insieme e il client le serve a due al secondo, una alla volta: il limitatore è nel client, non serve aggiungerne uno. Un client per processo L'autolimitazione conta le richieste di un client. Creandone uno per ogni corutina, ciascuno conta le proprie e nessuno conta il totale: cento corutine con cento client mandano cento richieste insieme, e sotto quel carico il servizio smette di rispondere. ```python # sbagliato async def leggi(urn): async with AsyncNormattiva() as n: # un client per chiamata return await n.dettaglio(urn) # giusto async def leggi(normattiva, urn): return await normattiva.dettaglio(urn) ``` ## Iniettare il proprio client HTTP Per metriche, tracing o intestazioni aggiuntive: ```python import httpx cliente = httpx.AsyncClient(event_hooks={"response": [misura]}) normattiva = AsyncNormattiva(http_client=cliente) ``` Un client iniettato dall'esterno sopravvive a `close()`: chiuderlo spetta a chi l'ha aperto. ## Quando conviene L'asincrono non rende le richieste più veloci: l'autolimitazione è la stessa. Usa `AsyncNormattiva` quando il programma ha altro da fare mentre aspetta, per esempio un servizio web che nel frattempo serve altre richieste. Per uno script che scarica e basta, il client sincrono fa la stessa cosa con meno codice. # Leggere il testo a una data Il testo di una legge cambia nel tempo. Normattiva conserva ogni versione, e `dettaglio` restituisce quella in vigore in un giorno preciso. ## Chiedere il testo di un giorno preciso ```python from datetime import date from normattiva import Normattiva with Normattiva() as normattiva: atto = normattiva.dettaglio( "urn:nir:stato:legge:1990-08-07;241~art19", vigenza=date(2000, 1, 1) ) print(atto.finestra) print(atto.finestra.inizio, atto.finestra.fine) print(atto.finestra.aperta) print(atto.testo[:60]) ``` ```text 1994-01-01 → 2005-03-07 1994-01-01 2005-03-07 False Art. 19 ((1. In tutti i casi in cui l'esercizio di un'attività ``` `vigenza` accetta un `datetime.date`. Il servizio non restituisce il testo «del 1° gennaio 2000»: restituisce la versione dell'articolo che quel giorno era in vigore, insieme all'intervallo in cui quella versione è rimasta valida. Quell'intervallo è la **finestra di vigenza**, ed è un FinestraVigenza: | Attributo | Tipo | Che cos'è | | ------------------ | -------------- | -------------------------------------------------------- | | `inizio` | `date` | il primo giorno in cui questa versione è stata in vigore | | `fine` | `date \| None` | l'ultimo giorno, oppure `None` se è ancora in vigore | | `aperta` | `bool` | `True` quando `fine` è `None` | | `contiene(giorno)` | `bool` | se quel giorno cade dentro la finestra | Nell'esempio la finestra va dal 1° gennaio 1994 al 7 marzo 2005: la data che abbiamo chiesto sta in mezzo, e nessuno dei due estremi coincide con essa. È normale, ed è l'informazione più utile della risposta: dice che quel testo era già in vigore da sei anni e lo sarebbe rimasto per altri cinque. Conserva la finestra insieme al testo Un testo salvato senza la sua finestra non è più interpretabile: fra sei mesi nessuno saprà a quale versione corrisponde. `finestra.inizio` è anche la data da ripassare a `dettaglio` per rileggere esattamente quella versione. ## Confrontare due date `dettaglio` va chiamato una volta per data. Le due chiamate sono indipendenti e si possono fare nello stesso blocco: ```python from datetime import date from normattiva import Normattiva URN = "urn:nir:stato:legge:1990-08-07;241~art19" with Normattiva() as normattiva: versioni = { anno: normattiva.dettaglio(URN, vigenza=date(anno, 1, 1)) for anno in (2000, 2015, 2024) } for anno, atto in versioni.items(): print(anno, atto.finestra, len(atto.testo), "caratteri") ``` ```text 2000 1994-01-01 → 2005-03-07 1618 caratteri 2015 2014-11-12 → 2015-08-27 6376 caratteri 2024 2020-05-19 → 2026-02-19 6198 caratteri ``` L'articolo 19 della legge 241, la segnalazione certificata di inizio attività, è passato da 1618 a oltre 6000 caratteri in vent'anni, e le tre versioni sono rimaste in vigore per periodi molto diversi: undici anni la prima, nove mesi la seconda. Una finestra senza fine è **aperta**: `fine` vale `None`, `aperta` vale `True` e la libreria la stampa come `oggi`. Nessuna delle tre qui sopra lo è, perché anche la versione del 2024 è stata poi sostituita. ## Il testo come fu pubblicato Al posto di una data, `vigenza` accetta la stringa `"originale"`: ```python originale = normattiva.dettaglio(URN, vigenza="originale") ``` Restituisce l'atto come è uscito in Gazzetta Ufficiale, prima di qualunque modifica. È l'unico valore non-data ammesso. ## Senza data si ottiene il testo di oggi ```python oggi = normattiva.dettaglio(URN) ``` La chiamata è legittima e non produce nessun avviso. Va però tenuto presente che nella risposta **non c'è niente** che dica «questo è il testo del giorno in cui l'hai chiesto»: `finestra.inizio` è la data dell'ultima modifica, che può essere di anni fa, e `finestra.fine` è `None`. Se il testo va conservato, la data di lettura va aggiunta da chi lo conserva: ```python salva( testo=oggi.testo, valido_dal=oggi.finestra.inizio, letto_il=date.today(), ) ``` ## Percorrere tutte le versioni `cronologia` restituisce le versioni una dopo l'altra, dalla prima pubblicazione a quella in vigore oggi: ```python for versione in normattiva.cronologia(URN, massimo=5): print(versione.finestra, len(versione.testo)) ``` ```python async for versione in normattiva.cronologia(URN, massimo=5): print(versione.finestra, len(versione.testo)) ``` ```text 1990-09-02 → 1992-06-10 2205 1992-06-11 → 1993-12-31 3001 1994-01-01 → 2005-03-07 1618 2005-03-08 → 2005-05-14 2440 2005-05-15 → 2009-07-03 3344 ``` Ogni elemento è un DettaglioAtto completo, con il testo e i commi di quella versione: `cronologia` è un iteratore, non un elenco di date. **Costa una richiesta per versione.** Il servizio non espone un elenco delle versioni di un articolo, quindi la libreria lo ricostruisce saltando di finestra in finestra: ```mermaid sequenceDiagram autonumber participant P as il tuo programma participant L as cronologia() participant S as servizio P->>L: cronologia(urn) L->>S: dettaglio(urn, vigenza="originale") S-->>L: testo, finestra 1990-09-02 → 1992-06-10 L-->>P: prima versione L->>S: dettaglio(urn, vigenza=1992-06-11) S-->>L: testo, finestra 1992-06-11 → 1993-12-31 L-->>P: seconda versione Note over L,S: e così via, un giorno dopo la fine di ciascuna finestra L->>S: dettaglio(urn, vigenza=2026-02-20) S-->>L: testo, finestra 2026-02-20 → aperta L-->>P: ultima versione, l'iterazione finisce ``` La catena si chiude quando arriva una finestra senza fine. L'articolo 19 ha 20 versioni, cioè 20 richieste, che alle due al secondo che la libreria si impone fanno una decina di secondi. Un atto intero, dove ogni articolo ha la sua storia, costa molto di più: per quello c'è l'esportazione. `massimo` ferma l'iterazione prima: ```python prime_cinque = list(normattiva.cronologia(URN, massimo=5)) ``` Senza `massimo` la catena si ferma dopo cinquecento passi Oltre quel numero `cronologia` solleva UnexpectedResponseError. Nessun articolo italiano ha cinquecento versioni: una catena così lunga vuol dire che le finestre hanno smesso di essere contigue, e la ricostruzione non troverebbe mai la fine. ## Quando la data cade fuori Due situazioni diverse, due errori diversi. **L'articolo non esisteva ancora.** Gli articoli aggiunti da una modifica successiva non hanno versioni prima di quella modifica: ```python from normattiva import NotYetInForceError, codici try: normattiva.dettaglio(codici.CODICE_PENALE.articolo("416bis"), vigenza=date(1975, 1, 1)) except NotYetInForceError as errore: print(errore) print(errore.vigente_dal) ``` ```text l'articolo non era ancora in vigore alla data richiesta (in vigore dal 1982-09-29) 1982-09-29 ``` L'articolo 416-bis del codice penale, l'associazione di tipo mafioso, è stato introdotto nel 1982 dalla legge Rognoni-La Torre: nel 1975 non esisteva. `vigente_dal` dice da quando esiste, quando il servizio manda l'informazione. **Il servizio ha risposto con la versione sbagliata.** La libreria controlla che la finestra restituita contenga davvero la data richiesta, e in caso contrario solleva ValidityMismatchError invece di restituire il testo. Oggi non capita: se capitasse, vorrebbe dire che il servizio ha cambiato comportamento e che i testi storici già raccolti vanno riguardati. ## Le date che il servizio accetterebbe Il servizio accetta date inesistenti, il 30 febbraio compreso, e invece di rifiutarle risponde qualcosa. Lavorando con oggetti `date` il problema non si pone, perché il 30 febbraio non è rappresentabile, e `Urn.parse` scarta le date impossibili prima di fare la richiesta. Restano scoperte solo le stringhe URN costruite a mano e usate altrove. ## Quando conviene l'esportazione `dettaglio` e `cronologia` lavorano su **un articolo alla volta**. Per un atto intero, con tutti i suoi articoli e tutte le loro versioni, una singola esportazione costa meno di centinaia di richieste, produce un archivio che si salva su disco e non tronca gli articoli lunghi, che sul percorso interattivo arrivano tagliati a cento commi. Vedi [esportare un atto intero](https://normattiva-sdk.ireneburresi.dev/come-fare/esportare-un-atto/index.md). # Usare la riga di comando Il pacchetto installa un comando che si chiama `normattiva`. Copre le stesse funzioni della libreria, senza scrivere Python: legge il testo di un atto, cerca nel corpus, percorre le versioni di un articolo, scarica un archivio. Conviene quando la domanda è una sola e la risposta si legge subito, o quando il risultato deve finire dentro un altro programma. Se invece stai costruendo qualcosa che fa molte richieste e ne combina i risultati, la libreria resta più comoda: la riga di comando non conserva oggetti fra un comando e il successivo. ```bash normattiva --help ``` ## Leggere il testo di un atto L'argomento `atto` è un URN: ```bash normattiva testo urn:nir:stato:legge:1990-08-07\;241 --articolo 19 ``` Il punto e virgola va protetto dalla shell, con la barra rovesciata come qui oppure mettendo tutto l'URN fra apici singoli. I dodici atti più citati si indicano per nome, e in quel caso l'allegato attraverso cui i loro articoli rispondono lo sceglie il comando: ```bash normattiva testo codice-civile --articolo 2043 ``` ```text REGIO DECRETO 16 marzo 1942, n. 262 Approvazione del testo del Codice civile. (042U0262) Citazione R.D. 16 marzo 1942, n. 262 Articolo 2043 Gazzetta G.U. n. 79 del 1942-04-04 Vigenza 1942-04-19 → oggi URN urn:nir:stato:regio.decreto:1942-03-16;262:2~art2043 Permalink https://www.normattiva.it/uri-res/N2Ls?urn:nir:stato:regio.decreto:1942-03-16;262:2~art2043 Art. 2043. (Risarcimento per fatto illecito). Qualunque fatto doloso o colposo, che cagiona ad altri un danno ingiusto, obbliga colui che ha commesso il fatto a risarcire il danno. Fonte: Normattiva (https://www.normattiva.it), Istituto Poligrafico e Zecca dello Stato, in licenza CC BY 4.0. Testo non autentico e gratuito: l'unico testo ufficiale è quello pubblicato sulla Gazzetta Ufficiale a mezzo stampa. ``` L'elenco completo dei nomi è `normattiva codici`, e la stessa tabella con la spiegazione degli allegati sta in [identificare un atto](https://normattiva-sdk.ireneburresi.dev/come-fare/identificare-un-atto/index.md). ### Il testo com'era a una certa data `--vigenza` prende un giorno, oppure la parola `originale`: ```bash normattiva testo urn:nir:stato:legge:1990-08-07\;241 \ --articolo 19 --vigenza 2000-01-01 ``` La riga `Vigenza` dell'intestazione dice in che finestra quel testo è stato in vigore: `1994-01-01 → 2005-03-07`, cioè da prima della data chiesta a dopo. Nessuna delle due date è quella che hai scritto tu, ed è normale: hai chiesto un istante, il servizio risponde con il tratto di tempo che lo contiene. ```bash normattiva testo urn:nir:stato:legge:1990-08-07\;241 \ --articolo 19 --vigenza originale ``` Il testo della prima pubblicazione in Gazzetta, prima di qualunque modifica. ```bash normattiva testo urn:nir:stato:legge:1990-08-07\;241 --articolo 19 ``` Senza `--vigenza` si ottiene il testo in vigore adesso. Nell'output nulla distingue questo caso da una data richiesta e non applicata, quindi per un testo storico la data va sempre scritta. ### Gli atti senza URN Per dodici tipi di atto su trenta, quasi tutti storici, la forma dell'URN non è verificata: chiederli per URN otterrebbe un 404 senza chiarirne la causa. Quegli atti si leggono dalle coordinate di Gazzetta, che una ricerca mostra sempre: ```bash normattiva testo --gazzetta 017U1234 --data 1917-05-20 ``` Quella strada però non supporta le date: risponde sempre con il testo di oggi. ## Cercare ```bash normattiva cerca procedimento amministrativo --anno 1990 ``` Le parole vengono combinate in AND dal servizio: non c'è modo di chiedere un OR né una frase esatta. Con `--faccette` la risposta mostra anche i valori con cui restringere, con accanto il nome dell'opzione che li accetta: ```bash normattiva cerca trasparenza --anno 1990 --faccette ``` ```text 8 atti trovati 1 D.L. 13 novembre 1990, n. 324 Provvedimenti urgenti in tema di lotta alla criminalita' organizzata e di trasparenza e buon andamento dell'attivita' amministrativa. urn:nir:stato:decreto.legge:1990-11-13;324 ... --tipo codice atti descrizione PPR 4 DECRETO DEL PRESIDENTE DELLA REPUBBLICA PLE 3 LEGGE PDL 1 DECRETO-LEGGE ``` Le faccette arrivano dentro la stessa risposta della ricerca, quindi non costano una richiesta in più. ### Una pagina, oppure tutte Senza `--massimo` si paga una richiesta sola e si ottiene una pagina, che si sfoglia con `--pagina` e `--per-pagina`. Con `--massimo` il comando scorre le pagine finché ha raccolto quel numero di atti, e quindi costa più richieste. ```bash normattiva cerca appalti --massimo 200 --json > appalti.json ``` ### Cercare per coordinate Quando l'atto lo sai già identificare, `cerca-avanzata` cerca il tipo, l'anno e il numero invece delle parole: ```bash normattiva cerca-avanzata --denominazione LEGGE --anno 1990 --numero 241 ``` I valori che `--denominazione` accetta li elenca il servizio: ```bash normattiva dizionario denominazioni ``` ## Percorrere le versioni di un articolo ```bash normattiva cronologia urn:nir:stato:legge:1990-08-07\;241 --articolo 19 --massimo 4 ``` ```text 4 versioni di urn:nir:stato:legge:1990-08-07;241~art19 1 1990-09-02 → 1992-06-10 2 1992-06-11 → 1993-12-31 3 1994-01-01 → 2005-03-07 4 2005-03-08 → 2005-05-14 ``` Costa una richiesta per versione, e l'articolo 19 della 241 ne ha venti: `--massimo` serve a non pagarle tutte quando ne bastano poche. La data che apre ogni finestra è quella da passare a `normattiva testo --vigenza` per rileggere quella versione. In JSON l'URN completo è già pronto in ogni voce, con il suffisso di vigenza attaccato. Non tutti gli articoli hanno un originale `cronologia` parte dalla prima pubblicazione. Un articolo inserito da una modifica successiva, come il 416-bis del codice penale, nel testo originale non c'era: il comando esce con `nessun atto per la richiesta` e il codice 3. Non è un difetto della richiesta, è la storia di quell'articolo. ## Scaricare un archivio `esporta` chiede al servizio un archivio con gli atti che i criteri trovano, attende che sia pronto e lo scrive su disco. I criteri sono gli stessi di `cerca-avanzata`. ```bash normattiva esporta --denominazione LEGGE --anno 1990 --numero 241 \ --archivio 241.zip --verboso ``` ```text normattiva: esportazione avviata, token 0fc601b0-da5c-4bb7-b717-4cfb6648015e normattiva: in attesa dell'archivio, al più 600 secondi normattiva: esportazione 0fc601b0-...: stato PROCESSING, 0/61 atti Archivio 241.zip Formato JSON Dimensione 1.6 MB Token 0fc601b0-da5c-4bb7-b717-4cfb6648015e ``` Il token viene scritto su stderr **prima** dell'attesa, che dura minuti: se il comando si interrompe, l'esportazione resta viva dalla parte del servizio e si riprende senza ricominciarla. ```bash normattiva esporta --token 0fc601b0-da5c-4bb7-b717-4cfb6648015e --archivio 241.zip ``` Prima di avviarla, il comando conta quanti atti prenderebbero i criteri, e oltre cento non parte. È il modo di accorgersi che un filtro prende mezzo corpus prima che il servizio ci lavori per un'ora. Il tetto si alza con `--massimo-atti`, oppure si toglie del tutto con `--senza-conteggio`, che salta anche la richiesta di conteggio. Alcuni archivi il servizio li tiene già pronti, e non c'è niente da attendere: ```bash normattiva collezioni normattiva scarica-collezione Codici --archivio codici.zip ``` ## Comporre un URN senza toccare la rete `urn` convalida un identificatore e lo scompone, oppure ne compone uno a partire dal nome di un atto noto. Non fa nessuna richiesta: se l'URN è malformato lo segnala subito, senza toccare la rete. ```bash normattiva urn codice-penale --articolo 416bis --vigenza 2010-01-01 ``` ```text urn:nir:stato:regio.decreto:1930-10-19;1398:1~art416bis!vig=2010-01-01 Autorità stato Denominazione regio.decreto Anno 1930 Data 1930-10-19 Numero 1398 Allegato 1 Articolo 416bis Versione 2010-01-01 Permalink https://www.normattiva.it/uri-res/N2Ls?urn:nir:stato:regio.decreto:1930-10-19;1398:1~art416bis!vig=2010-01-01 ``` L'allegato `1` non è stato dedotto: gli articoli del codice penale furono approvati come allegato al regio decreto e non rispondono sotto il decreto stesso. Quale allegato cambia da codice a codice, ed è una delle informazioni che `normattiva codici` conosce già. ## Passare il risultato a un altro programma Con `--json` l'output passa da testo impaginato a JSON, per ogni comando: ```bash normattiva testo codice-civile --articolo 2043 --json | jq -r .testo ``` L'output per il terminale manda a capo i capoversi alla larghezza della finestra; quello JSON porta il testo con le righe che il servizio ha mandato. La forma di ogni documento è descritta nel [riferimento della riga di comando](https://normattiva-sdk.ireneburresi.dev/riferimento/cli/#la-forma-del-json). ### Il codice di uscita dice che cosa è andato storto Dentro uno script si legge il codice, non il messaggio. Quello che serve più spesso è la distinzione fra `4`, la richiesta da correggere, e `5`, il servizio da riprovare più tardi; `3` vuol dire che l'atto non c'è. La tabella completa sta nel [riferimento](https://normattiva-sdk.ireneburresi.dev/riferimento/cli/#i-codici-di-uscita). ```bash if ! normattiva testo "$urn" --json > atto.json; then case $? in 3) echo "quell'atto non c'è" ;; 5) echo "servizio non disponibile, riprovo dopo" ;; esac fi ``` ## Colori e larghezza I colori compaiono solo quando l'output va a un terminale: redirigendo l'output in un file o in un altro programma spariscono da soli. Si forzano in un senso o nell'altro con `--colore sempre` e `--colore mai`, e la variabile d'ambiente `NO_COLOR` li disattiva senza bisogno di opzioni. Il testo viene mandato a capo alla larghezza della finestra, fino a un massimo di cento colonne, perché le righe più lunghe si leggono male. ## Vedere che cosa succede sotto `--verboso` manda su stderr i log della libreria: i retry, le attese dell'autolimitazione, gli stati di un'esportazione. Vanno su stderr, così l'output del comando resta pulito e si può ancora redirigere. ```bash normattiva cerca appalti --massimo 500 --verboso > appalti.txt ``` # Capire # Capire Che cosa sono gli oggetti che la libreria restituisce, com'è fatto il servizio che li produce e come la libreria si comporta quando quel servizio risponde male. Per scrivere le prime righe di codice bastano il [tutorial](https://normattiva-sdk.ireneburresi.dev/tutorial/index.md) e le guide di [come fare](https://normattiva-sdk.ireneburresi.dev/come-fare/index.md). | Pagina | A che domanda risponde | | -------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------- | | [Come funziona la normativa italiana](https://normattiva-sdk.ireneburresi.dev/capire/la-normativa-italiana/index.md) | chi fa le leggi, che rango hanno, come cambiano nel tempo, che cosa Normattiva contiene e che cosa no | | [Come è fatto un atto](https://normattiva-sdk.ireneburresi.dev/capire/come-e-fatto-un-atto/index.md) | che cos'è un comma, una rubrica, un decreto-legge, e perché lo stesso testo ha più versioni | | [Com'è fatto il servizio](https://normattiva-sdk.ireneburresi.dev/capire/il-servizio/index.md) | chi gestisce Normattiva, che licenza hanno i dati, perché i modelli sono due | | [Gli errori](https://normattiva-sdk.ireneburresi.dev/capire/errori/index.md) | la gerarchia delle eccezioni, e quando ha senso riprovare | | [L'affidabilità](https://normattiva-sdk.ireneburresi.dev/capire/affidabilita/index.md) | retry, autolimitazione, log, e come ci si accorge se l'API cambia | | [Perché la libreria fa così](https://normattiva-sdk.ireneburresi.dev/capire/scelte/index.md) | limiti che rifiutano, identificatori che non si indovinano, nomi metà in italiano | # L'affidabilità Il servizio è di terzi, gratuito, senza livelli di servizio garantiti e senza quote pubblicate. Può rallentare, rispondere male o non rispondere affatto. ## Verso il servizio **Due richieste al secondo, serializzate.** Il servizio non pubblica quote e non restituisce header di rate limit, quindi non esiste un limite ufficiale da rispettare: si sa solo che sotto raffica smette di rispondere e chiede di riprovare più tardi. Due al secondo è il valore prudente scelto da questa libreria, non un limite imposto da Normattiva, e puoi cambiarlo se il tuo caso lo giustifica: ```python Normattiva(requests_per_second=5.0) Normattiva(requests_per_second=0) # nessun limite ``` **Uno User-Agent identificante.** Chi riceve il traffico deve poter capire chi sei e come contattarti: ```python Normattiva(user_agent="il-mio-servizio/1.2 (+https://esempio.it/contatti)") ``` Il rate limiter è thread-safe, e la sua controparte asincrona usa un `asyncio.Lock`. Un client condiviso corrisponde a un solo budget di richieste verso il servizio. Il limite vale per client, non per programma Creando un client per chiamata, il rate limiting non limita più nulla: ogni client applica il proprio budget senza sapere degli altri. ## I retry `retries` è il numero di ritentativi dopo il primo tentativo: il predefinito 2 vuol dire al più tre richieste in tutto. Fra un tentativo e il successivo la libreria attende, e l'attesa raddoppia ogni volta a partire da mezzo secondo, più un po' di scarto casuale e con un tetto a otto secondi. ```mermaid stateDiagram-v2 direction LR [*] --> Attesa_del_turno Attesa_del_turno --> Richiesta : due al secondo Richiesta --> Riuscita : 2xx Richiesta --> Ritentabile : 400, 5xx, errore di rete Richiesta --> Definitiva : 409, 404, codice di regola Ritentabile --> Backoff : restano tentativi Backoff --> Attesa_del_turno : attesa raddoppiata Ritentabile --> Esaurita : nessun tentativo residuo Riuscita --> [*] Definitiva --> [*] : errore che descrive la richiesta Esaurita --> [*] : ConnectionError o UnexpectedResponseError ``` Che cosa viene ritentato non dipende dal solo codice di stato, perché in questo servizio il codice di stato è poco informativo: | Risposta | Ritentata? | Perché | | ---------------------------------- | ---------- | ------------------------------------------------------------------------------------- | | `400` | **sì** | il servizio non è deterministico: la stessa lettura può dare 200 al secondo tentativo | | `500`, `502`, `503`, `504` | **sì** | guasto del servizio | | `409` | no | è lo strato di protezione che rifiuta la forma della richiesta | | `4xx` con un codice di regola noto | no | descrive la richiesta: ripeterla non cambia niente | | errore di rete | **sì** | connessione azzerata, timeout | Tutte le chiamate che la libreria fa sono **letture**, quindi ripeterle è sicuro: non ci sono scritture che rischino di essere duplicate. ```python Normattiva(retries=0) # un tentativo solo, nessun ritentativo Normattiva(timeout=60.0) # per gli export lenti ``` Quando i tentativi si esauriscono, l'errore dipende da come il servizio ha risposto: | Cosa è arrivato | Errore finale | | ------------------------------------- | ------------------------------------------------------------------------------------- | | niente: connessione azzerata, timeout | `ConnectionError: il servizio non risponde: connessione azzerata` | | un `5xx` con un codice noto nel corpo | `ConnectionError: il servizio ha risposto 500: Errore generico, riprovare piu' tardi` | | un `5xx` senza codice riconoscibile | `UnexpectedResponseError: il servizio ha risposto 500: Internal Server Error` | Le risposte che non vengono ritentate diventano subito l'errore che le descrive: | Cosa è arrivato | Errore | | -------------------------------- | ------------------------------------------------------------------------------------------- | | `404` con il corpo applicativo | `NotFoundError: nessun atto per la richiesta` | | `409` dallo strato di protezione | `RequestBlockedError: la richiesta è stata respinta dai sistemi di protezione del servizio` | Quando il codice nel corpo cambia la decisione Un `400` con `code: 1501` (intervallo oltre dodici mesi) non viene ritentato: quel codice descrive la richiesta. Un `500` con `code: 1000` invece **viene** ritentato, perché il 1000 segnala un guasto del servizio e arriva anche per richieste perfettamente valide. ## Osservare cosa succede La libreria scrive log su un logger chiamato `normattiva`, a livello `DEBUG`: ```python import logging logging.basicConfig(level=logging.DEBUG) logging.getLogger("normattiva").setLevel(logging.DEBUG) ``` Su una richiesta che fallisce una volta e riesce alla seconda, il log mostra: ```text DEBUG:normattiva:il servizio ha risposto 500 su https://api.normattiva.it/.../atto/dettaglio-atto-urn DEBUG:normattiva:nuovo tentativo fra 0.51s su POST https://api.normattiva.it/.../atto/dettaglio-atto-urn ``` Il logger registra i retry e gli stati dell'esportazione. Per metriche e tracing conviene invece iniettare il proprio client HTTP e usare gli event hook di httpx: ```python import httpx Normattiva(http_client=httpx.Client(event_hooks={"response": [misura]})) ``` Un client iniettato dall'esterno non viene chiuso da `close()`: chiuderlo spetta a chi l'ha aperto. ## Il monitoraggio L'API di Normattiva non ha una specifica pubblicata a cui il servizio si impegni: può cambiare senza preavviso, e la libreria smetterebbe di leggere le risposte senza che nessuno lo sappia prima di chi la usa. Ogni notte una suite interroga tutti e quindici gli endpoint e confronta la forma delle risposte con un riferimento registrato; a uno scostamento si apre una issue sul repository. La stessa suite ricontrolla le anomalie note del servizio: verifica che si presentino ancora nello stesso modo, perché la libreria le gira intorno contando su quel comportamento. Il funzionamento del meccanismo è descritto in [il monitoraggio del contratto](https://normattiva-sdk.ireneburresi.dev/progetto/monitoraggio/index.md). # Come è fatto un atto I nomi che la libreria usa sono quelli del diritto italiano: atto, articolo, comma, rubrica, allegato, vigenza. Dello stesso testo, poi, esistono più versioni. Serve a leggere i dati, non a decidere una questione giuridica Quello che segue serve a leggere i dati con cognizione di causa. Per le conseguenze giuridiche di un testo, la fonte è la Gazzetta Ufficiale e l'interlocutore è un giurista. ## Le coordinate di un atto Un provvedimento si identifica con tre elementi: **che tipo di atto è**, **quando è stato emanato** e **che numero ha**. La libreria li raccoglie in EstremiAtto, che ogni modello espone nel campo `estremi`. ```python atto.estremi.denominazione # 'LEGGE' atto.estremi.data # date(1990, 8, 7) atto.estremi.numero # '241' atto.estremi.citazione # 'L. 7 agosto 1990, n. 241' ``` A questi si aggiunge un secondo gruppo, relativo alla **pubblicazione**. La **Gazzetta Ufficiale della Repubblica Italiana** è il giornale su cui lo Stato pubblica le leggi: un atto esiste come legge quando esce lì, e il testo stampato in Gazzetta è l'unico ufficiale. Esce quasi ogni giorno, numerata progressivamente per anno, e ha dei *supplementi* (ordinari e straordinari) per i testi lunghi. Un atto si individua quindi anche dalle sue coordinate di pubblicazione: su quale numero di Gazzetta è uscito, in che data, in quale supplemento, e con quale codice redazionale. ```python atto.gazzetta # G.U. n. 192 del 1990-08-18 atto.gazzetta.codice_redazionale # '090G0294' ``` Il **codice redazionale** è l'identificativo che l'IPZS assegna al singolo documento pubblicato. Non è un URN e non è leggibile, ma è l'unico identificatore disponibile per i dodici tipi di atto che una forma URN verificata non ce l'hanno. ## I tipi di atto Che cosa distingue una legge da un decreto-legge, da un decreto legislativo e da un regolamento, e come si ordinano fra loro, sta in [come funziona la normativa italiana](https://normattiva-sdk.ireneburresi.dev/capire/la-normativa-italiana/#chi-produce-le-norme). Qui basta sapere che il **tipo** fa parte dell'identità dell'atto: entra nella citazione, nell'URN e nei criteri di ricerca. ### Le abbreviazioni `citazione` scrive l'atto nella forma usata dai giuristi, e la libreria conosce undici abbreviazioni: | Denominazione | Abbreviazione | | --------------------------------------------------- | ------------- | | `COSTITUZIONE` | `Cost.` | | `LEGGE` | `L.` | | `LEGGE COSTITUZIONALE` | `L. cost.` | | `DECRETO-LEGGE` | `D.L.` | | `DECRETO LEGISLATIVO` | `D.Lgs.` | | `DECRETO DEL PRESIDENTE DELLA REPUBBLICA` | `D.P.R.` | | `DECRETO DEL PRESIDENTE DEL CONSIGLIO DEI MINISTRI` | `D.P.C.M.` | | `DECRETO MINISTERIALE` | `D.M.` | | `REGIO DECRETO` | `R.D.` | | `REGIO DECRETO-LEGGE` | `R.D.L.` | | `REGIO DECRETO LEGISLATIVO` | `R.D.Lgs.` | Il corpus di Normattiva contiene **trenta** denominazioni, molte delle quali storiche («decreto luogotenenziale», «decreto del capo provvisorio dello Stato»). Diciotto hanno una forma URN verificata, undici hanno un'abbreviazione, e le due liste non coincidono: vedi [identificare un atto](https://normattiva-sdk.ireneburresi.dev/come-fare/identificare-un-atto/#citare-un-atto). ## Come è fatto dentro Un atto è un albero. Il corpo dell'atto, la sequenza dei suoi articoli, si chiama **articolato**; le foglie rilevanti sono gli **articoli**, e dentro ciascuno il testo è diviso in **commi**. ```mermaid flowchart TD A[Atto] --> B["Partizioni superiori
libro, titolo, capo, sezione"] A --> G["Allegati
annessi"] B --> C["Articolo
numero + rubrica"] C --> D["Comma 1"] C --> E["Comma 2"] E --> F["lettere a), b), c)
numeri 1), 2), 3)"] ``` **Articolo.** L'unità numerata di cui è composto un atto. La `rubrica` è il suo titolo, quello fra parentesi: *«Conclusione del procedimento»*. Non tutti gli articoli ce l'hanno, e nelle versioni storiche spesso manca del tutto. **Comma.** Il capoverso numerato dentro un articolo. Quando si cita «l'articolo 2, comma 1» si intende il primo capoverso dell'articolo 2. Nel percorso interattivo la libreria li restituisce già separati: ```python atto.commi[0] # Comma(numero='1', testo="Ove il procedimento consegua obbligatoriamente ...") ``` Lo schema qui sotto mette il testo come lo stampa il servizio accanto ai campi che lo contengono: Le **lettere** e i **numeri** che spezzano un comma restano dentro il testo del comma: la libreria non li separa, perché il servizio non li marca in modo affidabile. Le **doppie parentesi** e le righe che cominciano con `$$` sono segni redazionali di Normattiva: le prime racchiudono il testo introdotto da una modifica, le seconde aprono le note di aggiornamento, che `DettaglioAtto` tiene in `note_aggiornamento` invece di lasciarle dentro `testo`. **Partizioni superiori.** Negli atti lunghi gli articoli sono raggruppati in capi, titoli, libri, sezioni. Servono a orientarsi, non a citare: un articolo si cita per numero, non per capo. **Allegati.** Testi che accompagnano l'atto senza farne parte come articolato. È qui che stanno i codici: il codice civile è l'allegato di un regio decreto, ed è [la ragione per cui i suoi articoli non rispondono sotto il decreto stesso](https://normattiva-sdk.ireneburresi.dev/come-fare/identificare-un-atto/#i-codici). ### Il bis, il ter, il quater Quando una modifica inserisce un articolo nuovo fra il 2 e il 3, gli articoli successivi non vengono rinumerati: si aggiunge un **2-bis**. Poi un 2-ter, un 2-quater, e così via con gli ordinali latini. In un atto esportato si vedono al loro posto: ```python for articolo in atto.vigente.articoli(): print(articolo.numero, "|", articolo.rubrica) ``` ```text 2 | Conclusione del procedimento 2 bis | Conseguenze per il ritardo dell'amministrazione nella conclusione del procedimento. 3 | Motivazione del provvedimento 3 bis | Uso della telematica. ``` Lo stesso vale per i commi. È il motivo per cui un articolo può avere 105 commi con l'ultimo etichettato «100», e per cui il numero di articolo è una **stringa** e non un intero: `"416bis"` è un numero di articolo del tutto normale. ## La vita di un atto nel tempo ```mermaid stateDiagram-v2 direction LR [*] --> Emanato : 7 agosto 1990 Emanato --> Pubblicato : G.U. del 18 agosto 1990 Pubblicato --> In_vacatio : 15 giorni In_vacatio --> In_vigore : 2 settembre 1990 In_vigore --> In_vigore : ogni novella apre una versione nuova In_vigore --> Abrogato : eventuale, e non cancella il testo Abrogato --> [*] ``` Fra la pubblicazione e l'entrata in vigore passa la **vacatio legis**: quindici giorni, salvo che la legge stessa disponga altrimenti (art. 73 Cost.). Nella legge 241 è verificabile direttamente: pubblicata il 18 agosto 1990, la prima finestra di vigenza del suo articolo 19 comincia il 2 settembre, quindici giorni dopo. Una **novella** è una modifica che un atto successivo apporta a un atto precedente: non un testo nuovo, ma un'istruzione di sostituzione applicata al testo esistente. È il motivo per cui la legge 241 del 1990 ha oggi 61 versioni diverse pur restando la stessa legge. L'**abrogazione** toglie efficacia a un atto per il futuro, senza cancellarlo: il testo abrogato resta consultabile, e resta applicabile ai fatti avvenuti mentre era in vigore. Per questo `AttoStorico.abrogato` è un'informazione, non un motivo per nascondere il testo. ### Le parole della vigenza Sono quattro, e nella libreria compaiono come nomi di campi e di parametri. **Vigenza** è l'essere in vigore di un testo. Un testo è vigente quando produce effetti giuridici. **Finestra di vigenza** è il periodo in cui una certa versione di un testo è stata quella in vigore: comincia il giorno in cui quella versione ha preso il posto della precedente e finisce il giorno prima che un'altra la sostituisca. Nella libreria è FinestraVigenza, e una finestra senza fine è quella tuttora in vigore. **Multivigenza** è la proprietà di una banca dati che conserva tutte le versioni succedutesi nel tempo, e non solo l'ultima. È la ragione per cui `dettaglio` accetta una data. **Testo originale** è la versione come è uscita in Gazzetta, prima di qualunque modifica; nella libreria si chiede con `vigenza="originale"`. La conseguenza pratica è che la domanda «cosa dice questo articolo» non ha risposta senza un «quando». Come si indica la data lo mostra [leggere il testo a una data](https://normattiva-sdk.ireneburresi.dev/come-fare/leggere-il-testo-a-una-data/index.md). ## Come si cita La forma canonica è *abbreviazione, giorno mese anno, n. numero*, seguita dall'articolo e dal comma quando servono. ```python from datetime import date from normattiva import EstremiAtto EstremiAtto("LEGGE", date(1990, 8, 7), "241").citazione # 'L. 7 agosto 1990, n. 241' ``` Nella pratica si scrive poi *«art. 2, comma 1, l. 241/1990»*. La libreria non compone questa forma estesa: si ferma alla citazione dell'atto, l'unica parte per cui esiste una convenzione davvero condivisa. # Gli errori Una libreria che dialoga con un servizio di terzi può fallire per due ragioni diverse: la richiesta era sbagliata, oppure il servizio non è riuscito a rispondere. Le due situazioni si gestiscono in modo opposto, e in questo servizio distinguerle guardando i codici di stato HTTP non è affidabile. Per questo la gerarchia degli errori segue una regola sola, senza eccezioni: > Ogni errore sollevato da questa libreria discende da `NormattivaError`. Quelli che indicano che **la richiesta era sbagliata** discendono anche da `ValueError`. Ne seguono i due `except` che coprono tutti i casi: ```python from normattiva import NormattivaError try: atto = normattiva.dettaglio(urn) except ValueError: ... # richiesta sbagliata: correggerla except NormattivaError: ... # errore del servizio, o risposta non interpretabile ``` Il secondo `except` cattura anche il primo, quindi l'ordine conta. ## La gerarchia ```mermaid classDiagram direction LR class ValueError { <> } class NormattivaError { <> } class InvalidArgumentError { un argomento non è valido } class InvalidUrnError { +str testo +str motivo } class RuleViolationError { +RuleCode regola +int codice } class NotFoundError { nessun atto per quelle coordinate } class AmbiguityError { +tuple candidati } class NotYetInForceError { +date vigente_dal } class TruncationError { +str ultimo_comma } class TooManyResultsError { +int totale +int massimo } class ConnectionError { il servizio non risponde } NormattivaError <|-- InvalidArgumentError NormattivaError <|-- InvalidUrnError NormattivaError <|-- RuleViolationError ValueError <|-- InvalidArgumentError ValueError <|-- InvalidUrnError ValueError <|-- RuleViolationError NormattivaError <|-- NotFoundError NotFoundError <|-- VersionNotFoundError NormattivaError <|-- AmbiguityError NormattivaError <|-- NotYetInForceError NormattivaError <|-- TruncationError NormattivaError <|-- ValidityMismatchError NormattivaError <|-- TooManyResultsError NormattivaError <|-- ExportFailedError NormattivaError <|-- OverloadedError NormattivaError <|-- RequestBlockedError NormattivaError <|-- UnexpectedResponseError NormattivaError <|-- ConnectionError ``` I tre errori in alto discendono **anche** da `ValueError`: sono quelli che descrivono una richiesta sbagliata, e la doppia discendenza è ciò che permette di prenderli tutti insieme con un `except ValueError` senza sapere quale strato li ha sollevati. ## Gli errori della richiesta Tre errori, tutti anche `ValueError`. ### `InvalidArgumentError` Un argomento non è valido, e per stabilirlo non serve interrogare il servizio. ```python normattiva.ricerca("procedimento", pagina=0) # InvalidArgumentError: pagina e per_pagina partono da 1 normattiva.dettaglio(urn_con_vigenza, vigenza=date(2005, 1, 1)) # InvalidArgumentError: l'URN chiede la vigenza ... e il parametro ne chiede ... esportazione.download() # su un export in formato AKN # InvalidArgumentError: il format AKN non viene letto in modelli: usare save() ``` Nessuno di questi casi genera traffico di rete. ### `InvalidUrnError` L'URN non rispetta la grammatica NIR, oppure appartiene a un tipo di atto la cui forma URN non è verificata. ```python errore.testo # quello che gli hai passato errore.motivo # perché non va bene, quando si sa ``` ### `RuleViolationError` La richiesta viola una regola dichiarata del servizio. In alcuni casi la violazione è segnalata dal servizio, in altri la libreria la rileva da sola. ```python from normattiva import RuleCode, RuleViolationError try: list(normattiva.atti_aggiornati(date(2020, 6, 1), date(2020, 1, 1))) except RuleViolationError as errore: errore.regola # RuleCode.DATE_INVERTITE errore.codice # 1503 ``` Quando il codice non è fra quelli conosciuti, `regola` vale `None`. I codici noti stanno in RuleCode. Descrive sempre la richiesta Se il codice arriva insieme a un `5xx`, la libreria solleva `ConnectionError`: un `5xx` indica un problema del servizio, non della richiesta, e arriva anche a richieste perfettamente valide. ## Gli errori restituiti dal servizio ### `NotFoundError` Nessun atto corrisponde a quelle coordinate. Può significare che l'atto non esiste, oppure che l'URN è malformato in un modo che la grammatica non intercetta, come succede agli articoli dei codici richiesti senza allegato. ### `AmbiguityError` L'URN corrisponde a più atti. `errore.candidati` li contiene, già letti. ### `NotYetInForceError` L'articolo non esisteva alla data richiesta. `errore.vigente_dal` indica da quando esiste, quando il servizio fornisce l'informazione. ### `OverloadedError` Il servizio ha rifiutato la richiesta perché sovraccarico. `errore.descrizione` riporta la spiegazione, quando il servizio ne dà una. ### `RequestBlockedError` Lo strato di protezione davanti all'API ha respinto la **forma** della richiesta. Non viene mai ritentato, perché la respingerebbe di nuovo. ## Gli errori sul testo ### `TruncationError` Sollevato solo su richiesta, con `se_troncato="solleva"`. `errore.ultimo_comma` è l'etichetta a cui il testo si interrompe. ### `ValidityMismatchError` Il servizio ha risposto con una versione che non copre la data richiesta. Oggi non capita: se capitasse, il servizio avrebbe cambiato comportamento e i testi storici già ottenuti andrebbero riguardati. ### `VersionNotFoundError` Nessuna versione di un `AttoStorico` copre la data richiesta, di solito perché è anteriore alla pubblicazione dell'atto. È una sottoclasse di `NotFoundError`: un `except NotFoundError` le intercetta entrambe. ### `TooManyResultsError` L'esportazione supererebbe il limite consentito. `errore.totale` e `errore.massimo` riportano il costo effettivo e il limite impostato. ## Gli errori di trasporto ### `ConnectionError` Il servizio non è raggiungibile, ha smesso di rispondere, oppure ha risposto `5xx` con un codice che dichiara un guasto (tipicamente il `1000`) fino a esaurire i tentativi. Ha senso riprovare più tardi. ```python try: normattiva.ricerca("appalti") except ConnectionError as errore: print(errore) ``` ```text il servizio non risponde: connessione azzerata il servizio ha risposto 500: Errore generico, riprovare piu' tardi ``` ### `UnexpectedResponseError` La risposta non ha la forma che la libreria sa interpretare. Copre anche un `5xx` senza alcun codice riconoscibile, dopo che i tentativi si sono esauriti: ```python try: normattiva.ricerca("appalti") except UnexpectedResponseError as errore: print(errore) # il servizio ha risposto 500: Internal Server Error ``` Se compare in modo sistematico su una richiesta ben formata, il contratto dell'API è cambiato: è il caso che il [monitoraggio](https://normattiva-sdk.ireneburresi.dev/capire/affidabilita/#il-monitoraggio) serve a scoprire in anticipo. ## Cosa fare, per categoria | Errore | Ha senso riprovare? | Cosa fare | | ------------------------- | ------------------- | ------------------------------------------------------------------------------------------------------ | | `InvalidArgumentError` | no | correggere il codice | | `InvalidUrnError` | no | correggere l'identificatore | | `RuleViolationError` | no | correggere i criteri | | `NotFoundError` | no | verificare le coordinate | | `AmbiguityError` | no | scegliere fra i candidati | | `TooManyResultsError` | no | restringere, o alzare il limite | | `ConnectionError` | **sì**, più tardi | la libreria ha già esaurito i suoi tentativi | | `OverloadedError` | **sì**, più tardi | il servizio è sotto carico | | `RequestBlockedError` | no | la forma della richiesta è respinta | | `UnexpectedResponseError` | dipende | se arriva da un `5xx`, riprovare più tardi; se è sistematico su una richiesta valida, aprire una issue | ## Perché non ci sono errori HTTP nudi La libreria non lascia propagare `httpx.HTTPStatusError`. Ogni risposta fallita viene letta e tradotta nell'eccezione che la descrive, in un solo punto del codice. In questo servizio il codice di stato è poco informativo: un `404` può arrivare come `200` con un elenco vuoto, e un `500` può significare «riprovare» oppure «la richiesta è impossibile», a seconda del corpo. Un `except httpx.HTTPStatusError` scritto a mano non avrebbe abbastanza informazioni per decidere come procedere. # Com'è fatto il servizio Diverse scelte della libreria discendono dalla forma del servizio: dalle due famiglie di modelli fino al fatto che un URN da solo non basti a identificare un atto. ## Chi lo gestisce, e con che licenza Normattiva è il portale della legge vigente dello Stato italiano. Il servizio è curato dall'[Istituto Poligrafico e Zecca dello Stato](https://www.ipzs.it) per conto della [Presidenza del Consiglio dei Ministri](https://www.governo.it), della Camera dei Deputati e del Senato della Repubblica. Accanto al portale di consultazione, IPZS pubblica lo stesso corpus come open data su [dati.normattiva.it](https://dati.normattiva.it): archivi già confezionati da scaricare e l'API HTTP con cui parla questa libreria, che risponde senza chiave e senza registrazione. L'apertura è avvenuta per fasi, e dal 1° gennaio 2026 copre tutti gli atti in tutte le versioni: le date e le licenze di ciascuna fase stanno in [Licenza e attribuzione](https://normattiva-sdk.ireneburresi.dev/progetto/licenza/#con-che-licenza). I dati sono in licenza [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/deed.it), quindi **l'attribuzione è obbligatoria**. Ogni modello della libreria la espone già pronta: ```python atto.attribuzione # 'Fonte: Normattiva (https://www.normattiva.it), Istituto Poligrafico e Zecca # dello Stato, in licenza CC BY 4.0. Testo non autentico e gratuito: l'unico # testo ufficiale è quello pubblicato sulla Gazzetta Ufficiale a mezzo stampa.' ``` «Testo non autentico» vuol dire che quello di Normattiva è una ricostruzione redazionale: le modifiche successive sono state applicate al testo originale da una redazione, che può sbagliare. In caso di divergenza prevale il testo stampato sulla Gazzetta Ufficiale. Deve saperlo anche chi legge quello che costruisci con questi dati: vedi [licenza e attribuzione](https://normattiva-sdk.ireneburresi.dev/progetto/licenza/index.md). ## L'API può cambiare L'indirizzo dell'API porta un `/v1`, ma quel numero non è un contratto: non esiste una specifica pubblicata a cui il servizio si impegni, né un preavviso per le modifiche. La forma delle risposte può cambiare sotto lo stesso numero di versione, e il servizio si è già mosso più volte. ```mermaid flowchart LR A["Fase sperimentale
fino al 30 giugno 2025
CC BY 4.0 NC,
funzionalità ridotte"] --> B["1° luglio 2025
cade la clausola NC
CC BY 4.0"] B --> C["1° gennaio 2026
corpus completo
originale, a una data,
multivigente"] C --> D["Oggi
nessuna specifica pubblicata:
può cambiare in qualsiasi momento
"] ``` Perché un cambiamento non si scopra da un programma che smette di funzionare, ogni notte una suite interroga tutti e quindici gli endpoint e confronta la forma delle risposte con un riferimento registrato; a uno scostamento si apre una issue sul repository. Il meccanismo è descritto in [l'affidabilità](https://normattiva-sdk.ireneburresi.dev/capire/affidabilita/#il-monitoraggio). ## Due modelli di risposta Il servizio ha due modelli di dati, e la libreria li tiene distinti. **Il percorso interattivo** (`dettaglio`, `cronologia`) restituisce il testo di **un atto o un articolo** in **una finestra di vigenza**. Arriva come HTML generato da Akoma Ntoso, lo standard XML dei documenti normativi, e la libreria lo scompone in testo piano, commi, note di aggiornamento e formula introduttiva. **L'esportazione** restituisce un **atto intero** con **tutte** le sue versioni, in un archivio ZIP di documenti JSON strutturati ad albero. ```mermaid flowchart LR A["dettaglio()
cronologia()"] --> B["HTML da Akoma Ntoso"] B --> C["DettaglioAtto
testo, commi, note,
una finestra di vigenza
"] D["start_export()"] --> E["ZIP di JSON
un file per versione"] E --> F["Corpus > AttoStorico
albero di partizioni,
tutte le versioni
"] ``` Un modello unico richiederebbe campi opzionali fuorvianti: il testo sarebbe presente per il percorso interattivo e assente per l'export, dove al suo posto c'è l'articolato. Le due famiglie di modelli restano separate perché le due risposte sono strutturalmente diverse. | | `DettaglioAtto` | `AttoStorico` | | ------------- | ----------------------------------------- | -------------------------------- | | Da dove | percorso interattivo | esportazione | | Cosa contiene | il testo di un atto o articolo a una data | l'atto intero, tutte le versioni | | Struttura | testo piano più commi | albero di partizioni | | Costo | una richiesta | circa un minuto | ## L'URN indirizza, non identifica Un URN NIR indirizza un atto in modo affidabile, ma non lo identifica in modo univoco: due provvedimenti distinti possono rispondere allo stesso URN, di solito perché lo stesso numero è stato assegnato a due atti pubblicati in Gazzette diverse. In quel caso il servizio restituisce l'elenco dei candidati al posto dell'atto, e la libreria lo trasforma in AmbiguityError, che contiene i candidati. ## Il testo è HTML, e le classi sono stabili Il campo `articoloHtml` è markup generato da Akoma Ntoso. I nomi di classe sono stabili, e questo permette di separare in modo affidabile quattro componenti: - il testo dell'articolo, in `testo` - le note redazionali di aggiornamento, accodate in fondo, in `note_aggiornamento` - la formula introduttiva, in testa, in `preambolo` - i commi numerati, in `commi` Senza questa separazione, `atto.testo` conterrebbe anche il testo delle note, e qualunque conteggio di parole o ricerca nel testo darebbe risultati falsati. ## Quattro formati di data, tre modi di dire «manca» Nello stesso servizio, a seconda dell'endpoint, una data può arrivare in quattro formati: ```text "1990-08-07" ISO "1990-08-07T00:00:00Z" ISO con istante "19900807" compatta "07/08/1990" italiana ``` Anche i valori assenti hanno più rappresentazioni: stringa vuota, `"0"`, e `"99999999"` quando la finestra non ha fine. La libreria le riconosce tutte e restituisce una `date` oppure `None`, in un solo punto del codice, così nessun altro modulo deve conoscere questi formati. ## Alcuni atti non dichiarano le proprie coordinate La Costituzione ha anno, mese, giorno e numero tutti a **zero**: non è un provvedimento numerato. La libreria legge quello zero come un'assenza e usa la data di Gazzetta. Sono valori che una `date` di Python non può rappresentare: senza questa lettura, la Costituzione sarebbe l'unico atto del corpus che la libreria non riesce a costruire. ## Gli endpoint L'API open data espone quindici endpoint. La corrispondenza fra metodi ed endpoint sta in [gli endpoint](https://normattiva-sdk.ireneburresi.dev/riferimento/endpoint/index.md). ### I criteri con due nomi La ricerca avanzata e l'esportazione accettano gli stessi criteri, ma tre campi cambiano nome da uno schema all'altro. Passare all'esportazione il nome usato dalla ricerca non produce errori: quel filtro viene ignorato silenziosamente. L'errore si scopre solo confrontando le due definizioni campo per campo, oppure esportando due volte e contando gli atti. La libreria traduce i nomi automaticamente. I tre campi sono elencati fra [gli endpoint](https://normattiva-sdk.ireneburresi.dev/riferimento/endpoint/#i-criteri-con-due-nomi). ## Cosa la libreria non espone Alcuni campi presenti nella specifica non hanno un parametro nella libreria: parametri che non hanno effetto, valori ammessi non documentati, un campo che manderebbe l'archivio per posta elettronica. L'elenco sta fra [gli endpoint](https://normattiva-sdk.ireneburresi.dev/riferimento/endpoint/#i-campi-non-esposti), il criterio con cui è stato compilato in [perché la libreria fa così](https://normattiva-sdk.ireneburresi.dev/capire/scelte/#che-cosa-non-viene-esposto). # Come funziona la normativa italiana Il sistema che Normattiva rappresenta: chi produce le norme, che forma hanno, come cambiano nel tempo e come si citano. Conoscerlo serve a leggere i dati con cognizione di causa; per le conseguenze giuridiche di un testo la fonte è la Gazzetta Ufficiale e l'interlocutore è un giurista. ## Che cosa c'è dentro Normattiva Il corpus contiene gli atti normativi **dello Stato italiano**, dal 1861 a oggi. Gli atti più vecchi sono regi decreti del Regno di Sardegna e poi del Regno d'Italia, numerati anche in cifre romane: ```python from datetime import date esito = normattiva.ricerca_avanzata(emanazione=(date(1861, 1, 1), date(1861, 12, 31))) print(esito.totale) print(esito.atti[0].citazione) ``` ```text 669 R.D. 8 dicembre 1861, n. 408 novies ``` Numeri come `408 novies` o `MDCCXIV` sono la ragione per cui in questa libreria il numero di un atto è una **stringa** e non un intero. Non ci sono, e vanno cercate altrove: | Che cosa | Dove sta | | ----------------------------------- | ------------------------------------ | | leggi e regolamenti **regionali** | banche dati delle singole Regioni | | diritto dell'**Unione europea** | [EUR-Lex](https://eur-lex.europa.eu) | | **sentenze** e altra giurisprudenza | banche dati giurisdizionali | | atti amministrativi non normativi | Gazzetta Ufficiale, parte seconda | ## Chi produce le norme Le fonti del diritto italiano sono ordinate in una gerarchia: quando due norme si contraddicono, prevale quella di rango superiore, e una norma di rango inferiore che contrasta con una superiore è invalida. ```mermaid flowchart TD A["Costituzione
e leggi costituzionali
art. 138 Cost."] B["Fonti primarie
legge, decreto-legge,
decreto legislativo,
referendum abrogativo"] C["Fonti secondarie
regolamenti governativi
e ministeriali
art. 17 l. 400/1988"] D["Consuetudine
solo dove la legge la richiama"] A --> B --> C --> D A -. "chi le fa" .-> A1["Parlamento,
con procedura aggravata"] B -.-> B1["Parlamento
oppure Governo"] C -.-> C1["Governo e ministri"] ``` Il **rango** non dipende dal contenuto dell'atto: dipende dal tipo di fonte e dal fondamento su cui l'atto è adottato. Un decreto legislativo del Governo ha lo stesso rango di una legge del Parlamento; un regolamento adottato con lo stesso strumento formale, il decreto del Presidente della Repubblica, ha rango inferiore. ## Le fonti primarie ### La legge ordinaria La funzione legislativa è esercitata **collettivamente dalle due Camere** (art. 70 Cost.): un testo diventa legge solo quando Camera e Senato ne approvano lo stesso identico articolato. ```mermaid flowchart LR A["Iniziativa
Governo, parlamentari,
popolo, Regioni
"] --> B["Camera"] B --> C["Senato"] C -- "modifiche" --> B C -- "stesso testo" --> D["Promulgazione
Presidente della Repubblica
entro un mese, art. 73
"] D --> E["Pubblicazione
in Gazzetta Ufficiale"] E --> F["Vacatio legis
15 giorni, salvo diverso termine"] F --> G["Entrata in vigore"] ``` Il passaggio avanti e indietro fra le due Camere si chiama *navetta*, e non ha un limite: la legge nasce solo quando le due Camere approvano un testo identico. La legge 241 del 1990 mostra le date reali di questo percorso: | Momento | Data | Dove si legge nella libreria | | ---------------------------- | ---------------- | --------------------------------------------------------- | | emanazione | 7 agosto 1990 | `atto.estremi.data` | | pubblicazione in G.U. n. 192 | 18 agosto 1990 | `atto.gazzetta.data` | | entrata in vigore | 2 settembre 1990 | `atto.finestra.inizio`, chiesto con `vigenza="originale"` | Fra pubblicazione ed entrata in vigore passano quindici giorni: è la **vacatio legis** dell'art. 73 Cost., il tempo in cui la legge esiste ma non si applica ancora. Alcune leggi la accorciano o la annullano dichiarando l'entrata in vigore «il giorno stesso della pubblicazione». ### Il decreto-legge Lo adotta il Governo in casi straordinari di necessità e urgenza (art. 77 Cost.). Entra in vigore subito, ma è **provvisorio**: se il Parlamento non lo converte in legge entro sessanta giorni, perde efficacia *sin dall'inizio*, come se non fosse mai esistito. ```mermaid stateDiagram-v2 [*] --> Adottato : il Governo delibera Adottato --> In_vigore_provvisorio : pubblicazione in G.U. In_vigore_provvisorio --> Convertito : legge di conversione entro 60 giorni In_vigore_provvisorio --> Convertito_con_modifiche : conversione che riscrive il testo In_vigore_provvisorio --> Decaduto : 60 giorni senza conversione Convertito --> [*] : il testo resta in vigore Convertito_con_modifiche --> [*] : vale il testo riscritto Decaduto --> [*] : perde efficacia sin dall'inizio ``` Il ramo di mezzo è quello che si incontra più spesso, ed è la ragione per cui i decreti-legge hanno molte versioni ravvicinate: il testo che leggi oggi è quello riscritto dalla legge di conversione, non quello adottato dal Governo. Con `vigenza="originale"` si ottiene il testo di partenza. ### Il decreto legislativo Lo adotta il Governo su **delega** del Parlamento (art. 76 Cost.). La legge delega deve fissare in anticipo l'oggetto, i principi e criteri direttivi e il termine entro cui il Governo può esercitarla. È la forma con cui si scrivono i testi lunghi e tecnici: il codice dell'amministrazione digitale, il codice dei contratti pubblici, il testo unico bancario. ### La legge costituzionale Modifica la Costituzione e segue la procedura aggravata dell'art. 138: doppia deliberazione di ciascuna Camera a distanza di almeno tre mesi, e se nella seconda votazione non si raggiungono i due terzi, possibilità di referendum confermativo. ## Le fonti secondarie I **regolamenti** non possono contraddire la legge: ne disciplinano l'attuazione e l'esecuzione. La legge 400 del 1988, all'articolo 17, disciplina i regolamenti **governativi** e quelli **ministeriali**: | Atto | Chi lo adotta | Sigla | | ------------------------ | ------------------------------------------------ | -------- | | regolamento governativo | Governo, emanato dal Presidente della Repubblica | `D.P.R.` | | regolamento ministeriale | un ministro | `D.M.` | Il decreto del Presidente del Consiglio (`D.P.C.M.`) non è fra i tipi dell'articolo 17: è la forma con cui il Presidente del Consiglio adotta atti propri, di regola amministrativi e in alcuni casi a contenuto normativo. La stessa sigla `D.P.R.` copre quindi atti di rango diverso: un D.P.R. può contenere un regolamento oppure, come nel caso di un testo unico, norme di rango primario adottate su delega. Il rango dipende dal fondamento su cui l'atto è stato adottato, non dalla sigla. ## Come cambiano le norme Un atto quasi mai viene sostituito in blocco: viene **modificato**, un pezzo alla volta, da atti successivi. **La novella** è la modifica che un atto nuovo apporta a un atto precedente. Non è un testo autonomo: è un'istruzione di sostituzione, del tipo «all'articolo 19, comma 1, le parole X sono sostituite dalle parole Y». Applicando tutte le novelle al testo originale si ottiene il **testo vigente**, che è quello che Normattiva ricostruisce e pubblica. ```mermaid flowchart LR O["Testo originale
come pubblicato in G.U."] --> V1["Versione 2"] N1["Atto modificante A
«ha disposto (con l'art. 4, comma 1)
la modifica dell'art. 6, comma 1»
"] -. novella .-> V1 V1 --> V2["Versione 3"] N2["Atto modificante B"] -. novella .-> V2 V2 --> V3["Testo vigente
quello che leggi oggi"] N3["Atto modificante C"] -. novella .-> V3 ``` Gli atti modificanti restano atti a sé: continuano a esistere, con il loro numero e la loro data, e quello che hanno prodotto è la nuova versione dell'atto modificato. `AttoStorico.aggiornamenti` contiene proprio queste istruzioni, con le parole del servizio, e `atti_aggiornati` elenca gli atti che ne hanno ricevuta una in un certo periodo. **L'abrogazione** toglie efficacia a una norma per il futuro. L'articolo 15 delle *preleggi*, cioè le Disposizioni sulla legge in generale premesse al codice civile, ne prevede tre forme: espressa, quando il legislatore lo dichiara; per incompatibilità, quando la norma nuova contraddice la vecchia; per nuova disciplina dell'intera materia. Un atto abrogato **resta consultabile** e resta applicabile ai fatti avvenuti mentre era in vigore, ed è la ragione per cui `AttoStorico.abrogato` è un'informazione e non un motivo per nascondere il testo. **La deroga** non abroga: lascia in piedi la norma generale e le sottrae dei casi. Per questo un testo può restare identico e cambiare significato quando altrove compare una norma derogatoria. Il risultato è che lo stesso articolo esiste in più versioni, ciascuna valida in un periodo. L'articolo 19 della legge 241, la segnalazione certificata di inizio attività, ne ha venti dal 1990 a oggi: Ogni gradino è una versione: l'altezza è la lunghezza del testo in caratteri, la larghezza è il tempo in cui quella versione è rimasta in vigore. Il crollo del 1994 è una riscrittura che accorciò l'articolo a 1618 caratteri; la crescita del 2005 in poi lo ha portato oltre i 6000. Il grafico si ottiene da `cronologia`, che restituisce le versioni una dopo l'altra: ```python for versione in normattiva.cronologia("urn:nir:stato:legge:1990-08-07;241~art19"): print(versione.finestra, len(versione.testo)) ``` Da qui la **multivigenza**, e la ragione per cui in questa libreria quasi ogni lettura accetta una data: vedi [leggere il testo a una data](https://normattiva-sdk.ireneburresi.dev/come-fare/leggere-il-testo-a-una-data/index.md). ## Testi unici e codici Quando una materia è regolata da decine di atti stratificati, il legislatore la raccoglie in un **testo unico**. Se il testo unico si limita a riordinare norme esistenti è *compilativo*; se le riscrive è *innovativo*, e in quel caso è esso stesso una fonte. I **codici** sono la forma più estesa di questa operazione. Codice civile, codice penale e codice di procedura civile sono stati approvati fra il 1930 e il 1942 con un regio decreto che li porta **in allegato**: il regio decreto contiene due o tre articoli di approvazione, e il codice vero e proprio è l'allegato. Il codice di procedura penale vigente è invece del 1988, adottato su delega nella forma del D.P.R. (D.P.R. 22 settembre 1988, n. 447): niente allegato, e i suoi articoli rispondono direttamente sotto quell'URN. Questa struttura è visibile nell'identificatore: l'articolo 2043 del codice civile non risponde sotto l'URN del R.D. 262/1942, ma sotto il suo allegato 2. ```python from normattiva import codici codici.CODICE_CIVILE.articolo(2043) # urn:nir:stato:regio.decreto:1942-03-16;262:2~art2043 # ↑ allegato 2 ``` Nello stesso regio decreto, l'allegato 1 contiene le preleggi: ```python normattiva.dettaglio("urn:nir:stato:regio.decreto:1942-03-16;262:1~art12").testo ``` ```text Art. 12. (Interpretazione della legge). Nell'applicare la legge non si può ad essa attribuire altro senso che quello fatto palese dal significato proprio delle parole secondo la connessione di esse, e dalla intenzione del legislatore. ... ``` I codici moderni, invece, sono decreti legislativi ordinari e non hanno allegati: il codice dell'amministrazione digitale è il D.Lgs. 82/2005, e i suoi articoli rispondono direttamente sotto quell'URN. ## Un atto per tipo, con il suo URN | Tipo | Atto | URN | | --------------------- | ------------------------------- | -------------------------------------------------------------------------------- | | Costituzione | Costituzione della Repubblica | `urn:nir:stato:costituzione:1947-12-27` | | Legge costituzionale | L. cost. 18 ottobre 2001, n. 3 | `urn:nir:stato:legge.costituzionale:2001-10-18;3` | | Legge | L. 7 agosto 1990, n. 241 | `urn:nir:stato:legge:1990-08-07;241` | | Decreto-legge | D.L. 17 marzo 2020, n. 18 | `urn:nir:stato:decreto.legge:2020-03-17;18` | | Decreto legislativo | D.Lgs. 7 marzo 2005, n. 82 | `urn:nir:stato:decreto.legislativo:2005-03-07;82` | | D.P.C.M. | D.P.C.M. 12 giugno 2026, n. 150 | `urn:nir:stato:decreto.del.presidente.del.consiglio.dei.ministri:2026-06-12;150` | | Decreto ministeriale | D.M. 25 ottobre 1999, n. 471 | `urn:nir:stato:decreto.ministeriale:1999-10-25;471` | | Regio decreto | R.D. 16 marzo 1942, n. 262 | `urn:nir:stato:regio.decreto:1942-03-16;262` | | Articolo di un codice | art. 2043 c.c. | `urn:nir:stato:regio.decreto:1942-03-16;262:2~art2043` | | Articolo con ordinale | art. 416-bis c.p. | `urn:nir:stato:regio.decreto:1930-10-19;1398:1~art416bis` | | Articolo a una data | art. 19 l. 241/1990 nel 2000 | `urn:nir:stato:legge:1990-08-07;241~art19!vig=2000-01-01` | Il D.P.R. 380/2001 è un caso di URN ambiguo Il testo unico dell'edilizia risponde a `urn:nir:stato:decreto.del.presidente.della.repubblica:2001-06-06;380`, ma quell'URN corrisponde a **due** atti pubblicati su Gazzette diverse: la G.U. 245 del 20 ottobre 2001 e la G.U. 266 del 15 novembre 2001. La libreria solleva AmbiguityError con i due candidati. Come si compone un URN pezzo per pezzo sta in [identificare un atto](https://normattiva-sdk.ireneburresi.dev/come-fare/identificare-un-atto/index.md); la struttura interna di un singolo atto in [come è fatto un atto](https://normattiva-sdk.ireneburresi.dev/capire/come-e-fatto-un-atto/index.md). # Perché la libreria fa così Alcune scelte di questa libreria sorprendono chi la usa per la prima volta: un limite che rifiuta invece di troncare, un identificatore che solleva un'eccezione anziché tentare, metà dei nomi in italiano e metà in inglese. ## Limitare o rifiutare Due parametri che sembrano fare la stessa cosa fanno l'opposto. Nella ricerca, `massimo` **limita**: l'iteratore scorre i risultati e si ferma dopo quel numero. Nell'esportazione, `massimo_atti` **rifiuta**: gli atti vengono contati prima di partire e, se sono più del limite, l'esportazione non parte affatto. La differenza sta in chi fa il lavoro. Scorrere una ricerca è lavoro di chi la chiede: fermarsi a metà risparmia qualche richiesta e non riguarda nessun altro. Un'esportazione è lavoro del servizio: dura minuti e una volta partita non si annulla, quindi un filtro che per sbaglio prende mezzo corpus impegna il servizio fino in fondo. Contare prima costa una richiesta, e con `massimo_atti=None` si può saltare. ## Non indovinare un identificatore Delle trenta denominazioni del corpus, diciotto rispondono a un URN composto secondo la regola standard. Per le altre dodici quella regola non funziona e la forma corretta non è nota. Per quelle, `AttoTrovato.urn` solleva InvalidUrnError invece di comporre un URN plausibile. Un identificatore inventato non fallisce in modo visibile: ottiene un `404`, che è la stessa risposta che il servizio dà a un atto inesistente. Chi lo riceve conclude che l'atto non c'è, e va a cercare altrove un testo che invece esiste; per questo la libreria preferisce sollevare un'eccezione. Lo stesso vale per gli allegati dei codici, che la libreria non deduce ma elenca uno per uno in [`codici`](https://normattiva-sdk.ireneburresi.dev/riferimento/codici/index.md), e per la `vigenza` chiesta a un atto raggiungibile solo dalle coordinate di Gazzetta: quel percorso le date non le conosce, e ignorare il parametro restituirebbe il testo di oggi facendolo passare per quello storico. ## Italiano e inglese nello stesso nome Il criterio di scelta è la distinzione fra dominio e tecnica, non la lingua in sé. Il dominio giuridico è in italiano: `dettaglio`, `vigenza`, `atto`, `comma`, `gazzetta`, `cronologia`. Tradurre `vigenza` vorrebbe dire inventare un termine che nessun giurista riconosce, e la parola inventata sarebbe più difficile da capire dell'originale, non meno. Lo strato tecnico è in inglese: `ConnectionError`, `retries`, `timeout`, `wait()`, `download()`, `ExportStatus`. Sono nomi con una forma canonica, che chi scrive Python riconosce da qualunque altra libreria; italianizzarli costringerebbe a impararli di nuovo. Le due convenzioni convivono nella stessa firma: `Export` ha un metodo `wait()` e restituisce `AttoStorico`; `dettaglio()` accetta `vigenza` e solleva `ConnectionError`. Messaggi d'errore e documentazione restano in italiano. ## Che cosa non viene esposto Alcuni campi che la specifica dell'API documenta non hanno un parametro nella libreria. L'elenco, con la ragione di ciascuno, sta fra [gli endpoint](https://normattiva-sdk.ireneburresi.dev/riferimento/endpoint/#i-campi-non-esposti). Il criterio è uno solo: un parametro viene esposto se si è visto che ha effetto. Un parametro accettato e ignorato dal servizio produce risultati sbagliati che sembrano giusti, e non c'è modo di accorgersene guardando la risposta; un parametro che non c'è si nota alla prima riga di codice. # Riferimento # Riferimento Classi, metodi, parametri ed eccezioni, uno per uno, con la firma esatta. | Pagina | Che cosa contiene | | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------ | | [Il client sincrono](https://normattiva-sdk.ireneburresi.dev/riferimento/client/index.md) | `Normattiva` | | [Il client asincrono](https://normattiva-sdk.ireneburresi.dev/riferimento/client-asincrono/index.md) | `AsyncNormattiva` | | [I modelli](https://normattiva-sdk.ireneburresi.dev/riferimento/modelli/index.md) | le forme che la libreria restituisce | | [Gli identificatori](https://normattiva-sdk.ireneburresi.dev/riferimento/urn/index.md) | `Urn` | | [L'esportazione](https://normattiva-sdk.ireneburresi.dev/riferimento/esportazione/index.md) | `Export`, `Corpus`, gli stati | | [Gli errori](https://normattiva-sdk.ireneburresi.dev/riferimento/errori/index.md) | la firma di ogni eccezione | | [Gli atti notissimi](https://normattiva-sdk.ireneburresi.dev/riferimento/codici/index.md) | `codici`, con l'allegato di ciascuno | | [Gli endpoint](https://normattiva-sdk.ireneburresi.dev/riferimento/endpoint/index.md) | quale metodo copre quale endpoint dell'API | | [La riga di comando](https://normattiva-sdk.ireneburresi.dev/riferimento/cli/index.md) | i comandi, i codici di uscita, la forma del JSON | Ogni errore è descritto per esteso in [gli errori](https://normattiva-sdk.ireneburresi.dev/capire/errori/index.md), che spiega anche la regola per catturarli tutti. # La riga di comando Il pacchetto installa un comando che si chiama `normattiva`: se hai installato `normattiva-sdk`, il comando c'è già. ```bash normattiva --versione python -m normattiva --versione ``` Le due forme sono equivalenti. La seconda serve dove lo script non è sul `PATH`, per esempio dentro un container in cui si invoca sempre l'interprete. Per i percorsi d'uso, con gli esempi e gli output reali, vedi [usare la riga di comando](https://normattiva-sdk.ireneburresi.dev/come-fare/usare-la-riga-di-comando/index.md). ## I comandi | Comando | Che cosa fa | Che cosa chiama | | -------------------- | -------------------------------------------------- | ---------------------------------------- | | `testo` | legge il testo di un atto o di un articolo | dettaglio, dettaglio_da_gazzetta | | `cerca` | cerca parole nel testo pieno | ricerca, ricerca_completa | | `cerca-avanzata` | cerca per coordinate | ricerca_avanzata | | `cronologia` | percorre le versioni di un articolo | cronologia | | `aggiornati` | elenca gli atti modificati fra due date | atti_aggiornati | | `esporta` | avvia un'esportazione e scrive l'archivio su disco | start_export, wait, save | | `collezioni` | elenca gli archivi già confezionati | collections | | `scarica-collezione` | scarica uno di quegli archivi | save_collection | | `dizionario` | elenca i codici che il servizio accetta | denominazioni e le altre due tipologiche | | `urn` | scompone o compone un identificatore | Urn, senza rete | | `codici` | elenca gli atti chiamabili per nome | codici, senza rete | `normattiva COMANDO --help` mostra le opzioni di ciascun comando, con un paio di esempi pronti all'uso. ### Che cosa non copre Il client asincrono, la lettura di un archivio in Corpus, `ricerche_predefinite` e l'iniezione di un client HTTP non hanno un comando corrispondente. ## Le opzioni comuni Valgono per ogni comando a cui si applicano, e vanno scritte dopo il nome del comando. | Opzione | Su | Che cosa fa | | ---------------------------- | ------------------------ | ------------------------------------------------------- | | `--json` | tutti | scrive un documento JSON invece del testo impaginato | | `--colore {auto,sempre,mai}` | tutti | `auto` colora solo se l'output è un terminale | | `--timeout SECONDI` | quelli che usano la rete | quanto attendere ogni singola risposta. Predefinito: 30 | | `--verboso` | quelli che usano la rete | scrive su stderr retry, attese e stati | La variabile d'ambiente `NO_COLOR`, se impostata, disattiva i colori senza bisogno di `--colore mai`. ## I codici di uscita | Codice | Nome | Quando | | ------ | -------------------- | ----------------------------------------------------------------------------------------------------------- | | 0 | `OK` | il comando è andato a buon fine | | 1 | `ERRORE` | errore non imputabile né alla richiesta né al servizio: tipicamente l'archivio non si è potuto scrivere | | 2 | `USO` | argomenti mancanti, malformati, o in contraddizione fra loro | | 3 | `NON_TROVATO` | `NotFoundError`, `VersionNotFoundError`, `NotYetInForceError` | | 4 | `RICHIESTA` | ogni altro errore della libreria: la richiesta era sbagliata | | 5 | `SERVIZIO` | `ConnectionError`, `UnexpectedResponseError`, `RequestBlockedError`, `OverloadedError`, `ExportFailedError` | | 130 | `INTERROTTO` | il processo ha ricevuto Ctrl-C | | 141 | `LETTURA_INTERROTTA` | il processo che leggeva l'output ha chiuso il canale, come fa `\| head` | I codici sono divisi per famiglia di causa e non per eccezione, perché è la distinzione che serve in uno script: su un `4` c'è da correggere la richiesta, su un `5` c'è da riprovare più tardi. Il messaggio va su stderr, sempre preceduto da `normattiva:`. ## La forma del JSON `--json` scrive un documento solo, indentato, con gli accenti non sfuggiti. `testo` produce l'atto: ```json { "citazione": "R.D. 16 marzo 1942, n. 262", "titolo": "REGIO DECRETO 16 marzo 1942, n. 262", "sottotitolo": "Approvazione del testo del Codice civile. (042U0262)", "urn": "urn:nir:stato:regio.decreto:1942-03-16;262:2~art2043", "permalink": "https://www.normattiva.it/uri-res/N2Ls?urn:nir:...", "estremi": {"denominazione": "REGIO DECRETO", "data": "1942-03-16", "numero": "262", "citazione": "..."}, "gazzetta": {"data": "1942-04-04", "numero": 79, "codice_redazionale": null, "supplemento": null, "numero_supplemento": null}, "vigenza": {"inizio": "1942-04-19", "fine": null}, "preambolo": null, "testo": "Art. 2043.\n(Risarcimento per fatto illecito).\n...", "commi": [], "note_aggiornamento": null, "possibile_troncamento": false, "fonte": "Fonte: Normattiva (https://www.normattiva.it), Istituto Poligrafico e Zecca dello Stato, in licenza CC BY 4.0. Testo non autentico e gratuito: l'unico testo ufficiale è quello pubblicato sulla Gazzetta Ufficiale a mezzo stampa." } ``` `urn` è `null` per i tipi di atto la cui forma URN non è verificata. Un identificatore composto a tentativi otterrebbe un `404`, che chi lo riceve leggerebbe come «l'atto non esiste». Gli altri comandi: | Comando | Chiavi di primo livello | | --------------------------------- | -------------------------------------------------------------------- | | `cerca`, `cerca-avanzata` | `totale`, `pagina`, `pagine`, `atti`, `faccette`, `fonte` | | `cerca --massimo N`, `aggiornati` | `atti`, `fonte` | | `cronologia` | `urn`, `versioni`, `fonte` | | `esporta` | `token`, `formato`, `archivio`, `byte`, `fonte` | | `scarica-collezione` | `archivio`, `byte`, `fonte` | | `collezioni` | `collezioni` | | `dizionario` | `denominazioni`, `classi` o `formati`, secondo quale è stato chiesto | | `urn` | le parti dell'identificatore, più `permalink` | | `codici` | `codici` | `cerca` cambia forma quando riceve `--massimo`, perché cambia la domanda: senza, si chiede una pagina e la risposta porta il totale e le faccette; con, si chiede un flusso di atti e il concetto di pagina non si applica. Ogni comando che produce dati di Normattiva include `fonte`, che è la stessa stringa di ATTRIBUZIONE. Nell'output per il terminale la stessa riga compare in fondo. La licenza dei dati la richiede, e la richiede anche a chi ripubblica quello che ha ottenuto da qui. ## I due formati di output | | terminale | `--json` | | -------------- | ------------------------------------------------------------------ | ----------------------------------- | | Testo | mandato a capo alla larghezza della finestra, fino a cento colonne | le righe che il servizio ha mandato | | Valori assenti | la riga non compare | la chiave c'è, con valore `null` | | Colore | solo se l'output è un terminale | mai | | Attribuzione | riga in fondo | chiave `fonte` | # Il client asincrono `AsyncNormattiva` rispecchia [`Normattiva`](https://normattiva-sdk.ireneburresi.dev/riferimento/client/index.md) metodo per metodo e firma per firma: cambiano `await` e `async for`, e gli argomenti del costruttore sono gli stessi, salvo `http_client`, che qui vuole un `httpx.AsyncClient`, e `sleep`, che passa da `asyncio.sleep`. Più corutine che condividono lo stesso client condividono anche la sua autolimitazione, quindi si mettono in fila da sole. Quando conviene usarlo lo spiega [lavorare in asincrono](https://normattiva-sdk.ireneburresi.dev/come-fare/lavorare-in-asincrono/index.md). ### AsyncNormattiva ```python AsyncNormattiva( *, user_agent: str | None = None, timeout: float = 30.0, retries: int = 2, requests_per_second: float = 2.0, base_url: str = PRODUZIONE, http_client: AsyncClient | None = None, sleep: Callable[[float], Awaitable[None]] = sleep, clock: Callable[[], float] = monotonic, ) ``` La variante asincrona del client. Ogni metodo rispecchia il suo gemello su `Normattiva`, firma compresa; quelli che iterano restituiscono iteratori asincroni. Anche gli argomenti del costruttore sono gli stessi, con `http_client` che qui vuole un `httpx.AsyncClient` e `sleep` che passa da `asyncio.sleep`. Le corutine che condividono un client condividono anche la sua autolimitazione, e quindi si mettono in fila da sole. Ogni metodo che tocca la rete può sollevare `ConnectionError` se il servizio non è raggiungibile, `UnexpectedResponseError` se la risposta non ha la forma che la libreria sa leggere, e `RequestBlockedError` se lo strato di protezione respinge la forma della richiesta. Le sezioni `Raises` dei singoli metodi elencano solo le eccezioni specifiche di ciascuno. Codice sorgente in `src/normattiva/client.py` ```python def __init__( self, *, user_agent: str | None = None, timeout: float = 30.0, retries: int = 2, requests_per_second: float = 2.0, base_url: str = PRODUZIONE, http_client: httpx.AsyncClient | None = None, sleep: Callable[[float], Awaitable[None]] = asyncio.sleep, clock: Callable[[], float] = time.monotonic, ) -> None: self._trasporto = TrasportoAsync( base_url=base_url, user_agent=user_agent, timeout=timeout, retries=retries, requests_per_second=requests_per_second, http_client=http_client, sleep=sleep, clock=clock, ) self._sleep = sleep self._clock = clock self._dizionari: dict[str, tuple[Tipologica, ...]] = {} ``` #### base_url ```python base_url: str ``` L'indirizzo base del servizio a cui questo client si rivolge. #### closed ```python closed: bool ``` Se questo client è stato chiuso. #### close ```python close() -> None ``` Rilascia il pool di connessioni, se è stato creato da questo client. Un client HTTP passato dall'esterno resta aperto: chiuderlo spetta a chi lo ha creato. Codice sorgente in `src/normattiva/client.py` ```python async def close(self) -> None: """Rilascia il pool di connessioni, se è stato creato da questo client. Un client HTTP passato dall'esterno resta aperto: chiuderlo spetta a chi lo ha creato. """ await self._trasporto.close() ``` #### dettaglio ```python dettaglio( atto: Urn | str | AttoTrovato, *, vigenza: Vigenza = None, se_troncato: SeTroncato = "segnala", ) -> DettaglioAtto ``` Legge il testo di un atto o di un suo articolo. `atto` è un URN, oppure un `AttoTrovato` uscito da una ricerca. Per i dodici tipi di atto su trenta la cui forma URN non è verificata la libreria passa dalle coordinate di Gazzetta, che il servizio accetta allo stesso modo. Quel percorso però non supporta le date: chiedere una `vigenza` per un atto raggiungibile solo così solleva un errore, il parametro non viene ignorato in silenzio. Senza `vigenza` il servizio restituisce il testo vigente oggi, e nella risposta nulla dichiara a quale data corrisponde: indicarla è l'unico modo di saperlo. Quando si indica una data, la finestra restituita viene verificata contro di essa. Con `se_troncato="solleva"` un articolo che sembra tagliato solleva `TruncationError` invece di limitarsi a segnalarlo in una proprietà. Parametri: | Nome | Tipo | Descrizione | Predefinito | | ------------- | ------------ | ---------------------------------------------------------------------------------------------------------- | ------------- | | `atto` | \`Urn | str | AttoTrovato\` | | `vigenza` | `Vigenza` | il giorno a cui leggere il testo, "originale" per la prima pubblicazione, None per il testo di oggi. | `None` | | `se_troncato` | `SeTroncato` | "segnala" registra il sospetto nella proprietà possibile_troncamento, "solleva" lo trasforma in eccezione. | `'segnala'` | Restituisce: | Tipo | Descrizione | | --------------- | --------------------------------------------------------------- | | `DettaglioAtto` | Il testo richiesto, con la finestra di vigenza in cui è valido. | Esempi: Il testo dell'articolo 19 della legge 241 com'era nel 2000, con la finestra in cui quel testo è stato in vigore:: ```text atto = normattiva.dettaglio( "urn:nir:stato:legge:1990-08-07;241~art19", vigenza=date(2000, 1, 1), ) print(atto.finestra) # 1994-01-01 → 2005-03-07 ``` Solleva: | Tipo | Descrizione | | ----------------------- | -------------------------------------------------------------------------------------------------------------- | | `NotFoundError` | nessun atto risponde a quelle coordinate. | | `AmbiguityError` | l'URN corrisponde a più atti pubblicati; i candidati sono elencati nell'eccezione. | | `NotYetInForceError` | l'articolo non esisteva alla data richiesta. | | `ValidityMismatchError` | il servizio ha risposto con una versione che non copre la data richiesta. | | `TruncationError` | solo con se_troncato="solleva", e solo se il testo sembra tagliato. | | `InvalidUrnError` | l'URN è malformato, oppure la forma URN di quel tipo di atto non è verificata. | | `InvalidArgumentError` | la vigenza è indicata due volte, oppure è chiesta per un atto raggiungibile solo dalle coordinate di Gazzetta. | Codice sorgente in `src/normattiva/client.py` ```python async def dettaglio( self, atto: Urn | str | AttoTrovato, *, vigenza: Vigenza = None, se_troncato: SeTroncato = "segnala", ) -> DettaglioAtto: """Legge il testo di un atto o di un suo articolo. `atto` è un URN, oppure un `AttoTrovato` uscito da una ricerca. Per i dodici tipi di atto su trenta la cui forma URN non è verificata la libreria passa dalle coordinate di Gazzetta, che il servizio accetta allo stesso modo. Quel percorso però non supporta le date: chiedere una `vigenza` per un atto raggiungibile solo così solleva un errore, il parametro non viene ignorato in silenzio. Senza `vigenza` il servizio restituisce il testo vigente oggi, e nella risposta nulla dichiara a quale data corrisponde: indicarla è l'unico modo di saperlo. Quando si indica una data, la finestra restituita viene verificata contro di essa. Con `se_troncato="solleva"` un articolo che sembra tagliato solleva `TruncationError` invece di limitarsi a segnalarlo in una proprietà. Args: atto: un URN, la sua forma testuale, oppure un `AttoTrovato` uscito da una ricerca. Il comma viene rimosso automaticamente, perché il servizio lo rifiuta in ingresso. vigenza: il giorno a cui leggere il testo, `"originale"` per la prima pubblicazione, `None` per il testo di oggi. se_troncato: `"segnala"` registra il sospetto nella proprietà `possibile_troncamento`, `"solleva"` lo trasforma in eccezione. Returns: Il testo richiesto, con la finestra di vigenza in cui è valido. Examples: Il testo dell'articolo 19 della legge 241 com'era nel 2000, con la finestra in cui quel testo è stato in vigore:: atto = normattiva.dettaglio( "urn:nir:stato:legge:1990-08-07;241~art19", vigenza=date(2000, 1, 1), ) print(atto.finestra) # 1994-01-01 → 2005-03-07 Raises: NotFoundError: nessun atto risponde a quelle coordinate. AmbiguityError: l'URN corrisponde a più atti pubblicati; i candidati sono elencati nell'eccezione. NotYetInForceError: l'articolo non esisteva alla data richiesta. ValidityMismatchError: il servizio ha risposto con una versione che non copre la data richiesta. TruncationError: solo con `se_troncato="solleva"`, e solo se il testo sembra tagliato. InvalidUrnError: l'URN è malformato, oppure la forma URN di quel tipo di atto non è verificata. InvalidArgumentError: la vigenza è indicata due volte, oppure è chiesta per un atto raggiungibile solo dalle coordinate di Gazzetta. """ _verifica_se_troncato(se_troncato) if isinstance(atto, AttoTrovato) and not atto.ha_urn: _verifica_senza_vigenza(atto, vigenza) codice, giorno = _coordinate_di_gazzetta(atto) return await self.dettaglio_da_gazzetta(codice, giorno, se_troncato=se_troncato) urn = atto.urn if isinstance(atto, AttoTrovato) else atto risposta = await self._trasporto.post( "atto/dettaglio-atto-urn", {"urn": str(_urn_con_vigenza(urn, vigenza))} ) return _controlla_dettaglio(_wire.leggi_dettaglio(risposta.json()), vigenza, se_troncato) ``` #### dettaglio_da_gazzetta ```python dettaglio_da_gazzetta( codice_redazionale: str, data: date, *, se_troncato: SeTroncato = "segnala", ) -> DettaglioAtto ``` Legge un atto a partire dalle sue coordinate di Gazzetta. È il percorso per gli atti la cui forma URN non è verificata: il codice redazionale e la data di Gazzetta arrivano da una ricerca, e il servizio risponde anche per un decreto-legge luogotenenziale del 1917. Questo percorso però non supporta la multivigenza: restituisce sempre il testo vigente oggi, e per una data serve un URN. Parametri: | Nome | Tipo | Descrizione | Predefinito | | -------------------- | ------------ | ----------------------------------------------------------------------------------------------- | -------------- | | `codice_redazionale` | `str` | l'identificativo di Gazzetta dell'atto, come arriva da AttoTrovato.gazzetta.codice_redazionale. | *obbligatorio* | | `data` | `date` | la data di pubblicazione in Gazzetta. | *obbligatorio* | | `se_troncato` | `SeTroncato` | come per dettaglio. | `'segnala'` | Restituisce: | Tipo | Descrizione | | --------------- | ------------------------------------------------------- | | `DettaglioAtto` | Il testo dell'atto, sempre nella versione vigente oggi. | Solleva: | Tipo | Descrizione | | ---------------------- | ---------------------------------------------- | | `NotFoundError` | nessun atto per quelle coordinate di Gazzetta. | | `InvalidArgumentError` | il codice redazionale è vuoto. | Codice sorgente in `src/normattiva/client.py` ```python async def dettaglio_da_gazzetta( self, codice_redazionale: str, data: date, *, se_troncato: SeTroncato = "segnala", ) -> DettaglioAtto: """Legge un atto a partire dalle sue coordinate di Gazzetta. È il percorso per gli atti la cui forma URN non è verificata: il codice redazionale e la data di Gazzetta arrivano da una ricerca, e il servizio risponde anche per un decreto-legge luogotenenziale del 1917. Questo percorso però non supporta la multivigenza: restituisce sempre il testo vigente oggi, e per una data serve un URN. Args: codice_redazionale: l'identificativo di Gazzetta dell'atto, come arriva da `AttoTrovato.gazzetta.codice_redazionale`. data: la data di pubblicazione in Gazzetta. se_troncato: come per `dettaglio`. Returns: Il testo dell'atto, sempre nella versione vigente oggi. Raises: NotFoundError: nessun atto per quelle coordinate di Gazzetta. InvalidArgumentError: il codice redazionale è vuoto. """ _verifica_se_troncato(se_troncato) risposta = await self._trasporto.post( "atto/dettaglio-atto", _corpo_gazzetta(codice_redazionale, data) ) return _controlla_dettaglio(_wire.leggi_dettaglio(risposta.json()), None, se_troncato) ``` #### cronologia ```python cronologia( urn: Urn | str, *, massimo: int | None = None ) -> AsyncIterator[DettaglioAtto] ``` Percorre tutte le versioni di un articolo, dall'originale a quella in vigore. Costa una richiesta per versione: le finestre di vigenza sono contigue, quindi ogni versione viene chiesta al giorno successivo alla fine della precedente. Con `massimo` l'iterazione si ferma dopo quel numero di versioni; senza, arriva all'ultima. Senza `massimo` la catena si ferma comunque dopo cinquecento passi: nessun articolo italiano ha cinquecento versioni, quindi una catena così lunga indica finestre non contigue. Parametri: | Nome | Tipo | Descrizione | Predefinito | | --------- | ----- | ----------- | ----------------------------------------------------------- | | `urn` | \`Urn | str\` | l'articolo di cui percorrere le versioni. | | `massimo` | \`int | None\` | quante versioni al più produrre. Senza, si arriva in fondo. | Produce: | Tipo | Descrizione | | ------------------------------ | ----------------------------------------------------------- | | `AsyncIterator[DettaglioAtto]` | Una versione per volta, dalla più vecchia alla più recente. | Solleva: | Tipo | Descrizione | | ------------------------- | ----------------------------------------------------------------------------------------------- | | `UnexpectedResponseError` | la catena non si chiude entro cinquecento passi, cioè le finestre di vigenza non sono contigue. | Codice sorgente in `src/normattiva/client.py` ```python async def cronologia( self, urn: Urn | str, *, massimo: int | None = None ) -> AsyncIterator[DettaglioAtto]: """Percorre tutte le versioni di un articolo, dall'originale a quella in vigore. Costa una richiesta per versione: le finestre di vigenza sono contigue, quindi ogni versione viene chiesta al giorno successivo alla fine della precedente. Con `massimo` l'iterazione si ferma dopo quel numero di versioni; senza, arriva all'ultima. Senza `massimo` la catena si ferma comunque dopo cinquecento passi: nessun articolo italiano ha cinquecento versioni, quindi una catena così lunga indica finestre non contigue. Args: urn: l'articolo di cui percorrere le versioni. massimo: quante versioni al più produrre. Senza, si arriva in fondo. Yields: Una versione per volta, dalla più vecchia alla più recente. Raises: UnexpectedResponseError: la catena non si chiude entro cinquecento passi, cioè le finestre di vigenza non sono contigue. """ base = Urn.parse(urn).senza_comma prossima: Vigenza = "originale" prodotte = 0 while massimo is None or prodotte < massimo: if massimo is None and prodotte >= PASSI_MASSIMI: raise UnexpectedResponseError( f"la catena delle versioni non si chiude dopo {PASSI_MASSIMI} passi: " "le finestre di vigenza non sono contigue" ) atto = await self.dettaglio(base.con_vigenza(prossima)) yield atto prodotte += 1 chiusura = atto.finestra.fine if atto.finestra else None if chiusura is None: return prossima = chiusura + timedelta(days=1) ``` #### ricerca ```python ricerca( testo: str, *, pagina: int = 1, per_pagina: int = 20, sort: Sort | str = NEWEST, tipo: str | None = None, anno: int | None = None, emettitore: str | None = None, ) -> EsitoRicerca ``` Cerca nel testo pieno del corpus. Le parole vengono combinate in AND dal servizio; non c'è modo di chiedere un OR. `tipo`, `anno` ed `emettitore` sono le faccette che la risposta stessa propone, non le coordinate dell'atto: per quelle c'è `ricerca_avanzata`. Parametri: | Nome | Tipo | Descrizione | Predefinito | | ------------ | ------ | ---------------------------------------------------- | --------------------------------------------------- | | `testo` | `str` | le parole da cercare, combinate in AND dal servizio. | *obbligatorio* | | `pagina` | `int` | quale pagina di risultati, a partire da 1. | `1` | | `per_pagina` | `int` | quanti risultati per pagina. | `20` | | `sort` | \`Sort | str\` | "newest" dal più recente, "oldest" dal più vecchio. | | `tipo` | \`str | None\` | codice della faccetta per tipo di atto, come "PLE". | | `anno` | \`int | None\` | faccetta per anno di provvedimento. | | `emettitore` | \`str | None\` | faccetta per amministrazione emanante. | Restituisce: | Tipo | Descrizione | | -------------- | --------------------------------------------------------------------- | | `EsitoRicerca` | Una pagina di risultati, con il totale e le faccette per restringere. | Codice sorgente in `src/normattiva/client.py` ```python async def ricerca( self, testo: str, *, pagina: int = 1, per_pagina: int = 20, sort: Sort | str = Sort.NEWEST, tipo: str | None = None, anno: int | None = None, emettitore: str | None = None, ) -> EsitoRicerca: """Cerca nel testo pieno del corpus. Le parole vengono combinate in AND dal servizio; non c'è modo di chiedere un OR. `tipo`, `anno` ed `emettitore` sono le faccette che la risposta stessa propone, non le coordinate dell'atto: per quelle c'è `ricerca_avanzata`. Args: testo: le parole da cercare, combinate in AND dal servizio. pagina: quale pagina di risultati, a partire da 1. per_pagina: quanti risultati per pagina. sort: `"newest"` dal più recente, `"oldest"` dal più vecchio. tipo: codice della faccetta per tipo di atto, come `"PLE"`. anno: faccetta per anno di provvedimento. emettitore: faccetta per amministrazione emanante. Returns: Una pagina di risultati, con il totale e le faccette per restringere. """ corpo = _corpo_ricerca( testo, pagina=pagina, per_pagina=per_pagina, sort=sort, tipo=tipo, anno=anno, emettitore=emettitore, ) risposta = await self._trasporto.post("ricerca/semplice", corpo) return _wire.leggi_ricerca(risposta.json()) ``` #### ricerca_avanzata ```python ricerca_avanzata( *, denominazione: str | None = None, anno: int | None = None, numero: int | str | None = None, giorno: int | None = None, mese: int | None = None, titolo: str | None = None, testo: str | None = None, vigente_al: date | None = None, classe: ClasseProvvedimento | int | None = None, emanazione: Intervallo | None = None, pubblicazione: Intervallo | None = None, sort: Sort | str = NEWEST, tipo: str | None = None, emettitore: str | None = None, pagina: int = 1, per_pagina: int = 20, ) -> EsitoRicerca ``` Cerca per coordinate invece che per parole. Senza nessuna coordinata il servizio risponde con l'intero corpus: è una richiesta ammessa, e la libreria non la rifiuta. Parametri: | Nome | Tipo | Descrizione | Predefinito | | --------------- | --------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------ | | `denominazione` | \`str | None\` | il tipo di atto come lo scrive il dizionario, per esempio "LEGGE" o "DECRETO LEGISLATIVO". | | `anno` | \`int | None\` | anno di emanazione. | | `numero` | \`int | str | None\` | | `giorno` | \`int | None\` | giorno di emanazione. | | `mese` | \`int | None\` | mese di emanazione. | | `titolo` | \`str | None\` | parole da cercare nel titolo. | | `testo` | \`str | None\` | parole da cercare nel testo. | | `vigente_al` | \`date | None\` | tiene solo gli atti in vigore in quel giorno. | | `classe` | \`ClasseProvvedimento | int | None\` | | `emanazione` | \`Intervallo | None\` | intervallo di emanazione, come coppia (dal, al). Un estremo può essere None per lasciare la finestra aperta. | | `pubblicazione` | \`Intervallo | None\` | intervallo di pubblicazione in Gazzetta, come sopra. | | `sort` | \`Sort | str\` | "newest" dal più recente, "oldest" dal più vecchio. | | `tipo` | \`str | None\` | faccetta per tipo di atto. | | `emettitore` | \`str | None\` | faccetta per amministrazione emanante. | | `pagina` | `int` | quale pagina di risultati, a partire da 1. | `1` | | `per_pagina` | `int` | quanti risultati per pagina. | `20` | Restituisce: | Tipo | Descrizione | | -------------- | -------------------------------------------------------------- | | `EsitoRicerca` | Una pagina di risultati, nella stessa forma che rende ricerca. | Codice sorgente in `src/normattiva/client.py` ```python async def ricerca_avanzata( self, *, denominazione: str | None = None, anno: int | None = None, numero: int | str | None = None, giorno: int | None = None, mese: int | None = None, titolo: str | None = None, testo: str | None = None, vigente_al: date | None = None, classe: ClasseProvvedimento | int | None = None, emanazione: Intervallo | None = None, pubblicazione: Intervallo | None = None, sort: Sort | str = Sort.NEWEST, tipo: str | None = None, emettitore: str | None = None, pagina: int = 1, per_pagina: int = 20, ) -> EsitoRicerca: """Cerca per coordinate invece che per parole. Senza nessuna coordinata il servizio risponde con l'intero corpus: è una richiesta ammessa, e la libreria non la rifiuta. Args: denominazione: il tipo di atto come lo scrive il dizionario, per esempio `"LEGGE"` o `"DECRETO LEGISLATIVO"`. anno: anno di emanazione. numero: numero del provvedimento. giorno: giorno di emanazione. mese: mese di emanazione. titolo: parole da cercare nel titolo. testo: parole da cercare nel testo. vigente_al: tiene solo gli atti in vigore in quel giorno. classe: la classe redazionale dell'atto (senza aggiornamenti, aggiornato, abrogato). emanazione: intervallo di emanazione, come coppia `(dal, al)`. Un estremo può essere `None` per lasciare la finestra aperta. pubblicazione: intervallo di pubblicazione in Gazzetta, come sopra. sort: `"newest"` dal più recente, `"oldest"` dal più vecchio. tipo: faccetta per tipo di atto. emettitore: faccetta per amministrazione emanante. pagina: quale pagina di risultati, a partire da 1. per_pagina: quanti risultati per pagina. Returns: Una pagina di risultati, nella stessa forma che rende `ricerca`. """ corpo = _corpo_avanzata( _coordinate( denominazione, anno, numero, giorno, mese, titolo, testo, vigente_al, classe, emanazione, pubblicazione, ), sort, pagina, per_pagina, tipo, emettitore, ) risposta = await self._trasporto.post("ricerca/avanzata", corpo) return _wire.leggi_ricerca(risposta.json()) ``` #### ricerca_completa ```python ricerca_completa( testo: str, *, massimo: int | None = None, per_pagina: int = 50, sort: Sort | str = NEWEST, tipo: str | None = None, anno: int | None = None, emettitore: str | None = None, ) -> AsyncIterator[AttoTrovato] ``` Scorre tutti gli atti che una ricerca trova, una pagina per volta. L'iteratore è pigro: ogni pagina viene chiesta solo quando serve, quindi consumare dieci risultati costa una richiesta sola. Con `massimo` ci si ferma dopo quel numero di atti; per sapere quanti ce n'erano in tutto basta una `ricerca` e il suo `totale`. Quel `totale` è anche la condizione di uscita: il numero di pagina restituito dal servizio non è affidabile, e usarlo come condizione potrebbe rileggere la prima pagina all'infinito. Parametri: | Nome | Tipo | Descrizione | Predefinito | | ------------ | ------ | ---------------------------------------- | ------------------------------------------------------- | | `testo` | `str` | le parole da cercare. | *obbligatorio* | | `massimo` | \`int | None\` | quanti atti al più produrre. Senza, si arriva in fondo. | | `per_pagina` | `int` | quanti risultati chiedere per richiesta. | `50` | | `sort` | \`Sort | str\` | "newest" dal più recente, "oldest" dal più vecchio. | | `tipo` | \`str | None\` | faccetta per tipo di atto. | | `anno` | \`int | None\` | faccetta per anno di provvedimento. | | `emettitore` | \`str | None\` | faccetta per amministrazione emanante. | Produce: | Tipo | Descrizione | | ---------------------------- | ----------------------------------------------------------- | | `AsyncIterator[AttoTrovato]` | Un atto per volta, nell'ordine in cui il servizio li rende. | Codice sorgente in `src/normattiva/client.py` ```python async def ricerca_completa( self, testo: str, *, massimo: int | None = None, per_pagina: int = 50, sort: Sort | str = Sort.NEWEST, tipo: str | None = None, anno: int | None = None, emettitore: str | None = None, ) -> AsyncIterator[AttoTrovato]: """Scorre tutti gli atti che una ricerca trova, una pagina per volta. L'iteratore è pigro: ogni pagina viene chiesta solo quando serve, quindi consumare dieci risultati costa una richiesta sola. Con `massimo` ci si ferma dopo quel numero di atti; per sapere quanti ce n'erano in tutto basta una `ricerca` e il suo `totale`. Quel `totale` è anche la condizione di uscita: il numero di pagina restituito dal servizio non è affidabile, e usarlo come condizione potrebbe rileggere la prima pagina all'infinito. Args: testo: le parole da cercare. massimo: quanti atti al più produrre. Senza, si arriva in fondo. per_pagina: quanti risultati chiedere per richiesta. sort: `"newest"` dal più recente, `"oldest"` dal più vecchio. tipo: faccetta per tipo di atto. anno: faccetta per anno di provvedimento. emettitore: faccetta per amministrazione emanante. Yields: Un atto per volta, nell'ordine in cui il servizio li rende. """ if massimo is not None and massimo <= 0: return prodotti = 0 pagina = 1 dichiarati: int | None = None quanti_per_pagina = per_pagina if massimo is None else min(per_pagina, massimo) while True: esito = await self.ricerca( testo, pagina=pagina, per_pagina=quanti_per_pagina, sort=sort, tipo=tipo, anno=anno, emettitore=emettitore, ) if dichiarati is None: dichiarati = esito.totale for atto in esito.atti: yield atto prodotti += 1 if massimo is not None and prodotti >= massimo: return if not esito.atti or prodotti >= dichiarati or pagina >= PAGINE_MASSIME: return pagina += 1 ``` #### atti_aggiornati ```python atti_aggiornati( dal: date, al: date ) -> AsyncIterator[AttoTrovato] ``` Elenca gli atti modificati fra due date. Il flusso contiene solo le modifiche: un atto pubblicato dentro la finestra ma mai modificato dopo non compare. Le finestre più lunghe di un anno vengono spezzate, perché il servizio le rifiuta. Parametri: | Nome | Tipo | Descrizione | Predefinito | | ----- | ------ | --------------------------------------- | -------------- | | `dal` | `date` | primo giorno della finestra, compreso. | *obbligatorio* | | `al` | `date` | ultimo giorno della finestra, compreso. | *obbligatorio* | Produce: | Tipo | Descrizione | | ---------------------------- | ----------------------------------------------------- | | `AsyncIterator[AttoTrovato]` | Un atto modificato per volta, finestra dopo finestra. | Solleva: | Tipo | Descrizione | | -------------------- | --------------------------------------------------------------------------------------- | | `RuleViolationError` | al precede dal. Sollevata prima di toccare la rete, col codice RuleCode.DATE_INVERTITE. | Codice sorgente in `src/normattiva/client.py` ```python async def atti_aggiornati(self, dal: date, al: date) -> AsyncIterator[AttoTrovato]: """Elenca gli atti modificati fra due date. Il flusso contiene solo le modifiche: un atto pubblicato dentro la finestra ma mai modificato dopo non compare. Le finestre più lunghe di un anno vengono spezzate, perché il servizio le rifiuta. Args: dal: primo giorno della finestra, compreso. al: ultimo giorno della finestra, compreso. Yields: Un atto modificato per volta, finestra dopo finestra. Raises: RuleViolationError: `al` precede `dal`. Sollevata prima di toccare la rete, col codice `RuleCode.DATE_INVERTITE`. """ _verifica_intervallo(dal, al) for inizio, fine in _intervalli(dal, al): risposta = await self._trasporto.post( "ricerca/aggiornati", _corpo_aggiornati(inizio, fine) ) for atto in _wire.leggi_ricerca(risposta.json()).atti: yield atto ``` #### denominazioni ```python denominazioni( *, reload: bool = False ) -> tuple[Tipologica, ...] ``` Elenca i tipi di atto che il corpus contiene, e tiene il risultato in memoria. Codice sorgente in `src/normattiva/client.py` ```python async def denominazioni(self, *, reload: bool = False) -> tuple[Tipologica, ...]: """Elenca i tipi di atto che il corpus contiene, e tiene il risultato in memoria.""" return await self._dizionario( "denominazioni", "tipologiche/denominazione-atto", _wire.leggi_denominazioni, reload ) ``` #### classi_provvedimento ```python classi_provvedimento( *, reload: bool = False ) -> tuple[Tipologica, ...] ``` Elenca le classi redazionali a cui un atto può appartenere. Codice sorgente in `src/normattiva/client.py` ```python async def classi_provvedimento(self, *, reload: bool = False) -> tuple[Tipologica, ...]: """Elenca le classi redazionali a cui un atto può appartenere.""" return await self._dizionario( "classi", "tipologiche/classe-provvedimento", _wire.leggi_classi, reload ) ``` #### export_formats ```python export_formats( *, reload: bool = False ) -> tuple[Tipologica, ...] ``` Elenca i formati in cui si può chiedere un'esportazione. Codice sorgente in `src/normattiva/client.py` ```python async def export_formats(self, *, reload: bool = False) -> tuple[Tipologica, ...]: """Elenca i formati in cui si può chiedere un'esportazione.""" return await self._dizionario( "estensioni", "tipologiche/estensioni", _wire.leggi_estensioni, reload ) ``` #### ricerche_predefinite ```python ricerche_predefinite() -> tuple[RicercaPredefinita, ...] ``` Elenca le ricerche predefinite che il servizio propone. Codice sorgente in `src/normattiva/client.py` ```python async def ricerche_predefinite(self) -> tuple[RicercaPredefinita, ...]: """Elenca le ricerche predefinite che il servizio propone.""" risposta = await self._trasporto.get("ricerca/predefinita") return _wire.leggi_ricerche_predefinite(risposta.json()) ``` #### collections ```python collections() -> tuple[Collection, ...] ``` Elenca gli archivi già confezionati che il servizio mette a disposizione. Codice sorgente in `src/normattiva/client.py` ```python async def collections(self) -> tuple[Collection, ...]: """Elenca gli archivi già confezionati che il servizio mette a disposizione.""" risposta = await self._trasporto.get("collections/collection-predefinite") return _wire.leggi_collezioni(risposta.json()) ``` #### download_collection ```python download_collection( name: str, *, format: Format | str = JSON, mode: ExportMode | str = VIGENTE, ) -> Corpus ``` Scarica un archivio già confezionato e legge gli atti che contiene. Solleva: | Tipo | Descrizione | | ---------------------- | ------------------------------------------------------------------------------------------ | | `InvalidArgumentError` | il formato chiesto non viene letto in modelli; usare save_collection per averlo come file. | Codice sorgente in `src/normattiva/client.py` ```python async def download_collection( self, name: str, *, format: Format | str = Format.JSON, mode: ExportMode | str = ExportMode.VIGENTE, ) -> Corpus: """Scarica un archivio già confezionato e legge gli atti che contiene. Raises: InvalidArgumentError: il formato chiesto non viene letto in modelli; usare `save_collection` per averlo come file. """ _verifica_leggibile(Format(format), "save_collection()") return Corpus.from_data(await self._archivio_collezione(name, format, mode)) ``` #### save_collection ```python save_collection( name: str, path: str | Path, *, format: Format | str = JSON, mode: ExportMode | str = VIGENTE, ) -> Path ``` Scarica su disco un archivio già confezionato, in qualunque formato sia. Codice sorgente in `src/normattiva/client.py` ```python async def save_collection( self, name: str, path: str | Path, *, format: Format | str = Format.JSON, mode: ExportMode | str = ExportMode.VIGENTE, ) -> Path: """Scarica su disco un archivio già confezionato, in qualunque formato sia.""" destinazione = Path(path) destinazione.write_bytes(await self._archivio_collezione(name, format, mode)) return destinazione ``` #### start_export ```python start_export( *, format: Format | str = JSON, mode: ExportMode | str = MULTIVIGENTE, massimo_atti: int | None = 100, escludi_testo: str | None = None, escludi_titolo: str | None = None, denominazione: str | None = None, anno: int | None = None, numero: int | str | None = None, giorno: int | None = None, mese: int | None = None, titolo: str | None = None, testo: str | None = None, vigente_al: date | None = None, classe: ClasseProvvedimento | int | None = None, emanazione: Intervallo | None = None, pubblicazione: Intervallo | None = None, ) -> AsyncExport ``` Chiede l'esportazione completa degli atti che una ricerca trova. Le coordinate sono le stesse di `ricerca_avanzata`. Prima di avviare l'esportazione gli atti vengono contati con una ricerca sincrona, perché un'esportazione costa minuti di lavoro al servizio e, una volta partita, non si annulla. Con `massimo_atti=None` si parte senza conteggio. `escludi_testo` ed `escludi_titolo` escludono dal risultato gli atti che contengono quelle parole, e sono l'unico filtro che l'esportazione supporta e la ricerca no: il conteggio preventivo non ne tiene conto, quindi può contare più atti di quanti ne arriveranno. Parametri: | Nome | Tipo | Descrizione | Predefinito | | ---------------- | --------------------- | ----------- | ------------------------------------------------------------------------------------------- | | `format` | \`Format | str\` | in che formato produrre l'archivio. Solo JSON viene poi letto in modelli. | | `mode` | \`ExportMode | str\` | quante versioni includere nell'archivio. | | `massimo_atti` | \`int | None\` | il tetto oltre il quale l'esportazione non parte. None la avvia senza conteggio preventivo. | | `escludi_testo` | \`str | None\` | esclude gli atti che contengono questa parola. | | `escludi_titolo` | \`str | None\` | esclude gli atti il cui titolo la contiene. | | `denominazione` | \`str | None\` | come in ricerca_avanzata, e così tutti i criteri che seguono. | | `anno` | \`int | None\` | anno di emanazione. | | `numero` | \`int | str | None\` | | `giorno` | \`int | None\` | giorno di emanazione. | | `mese` | \`int | None\` | mese di emanazione. | | `titolo` | \`str | None\` | parole da cercare nel titolo. | | `testo` | \`str | None\` | parole da cercare nel testo. | | `vigente_al` | \`date | None\` | tiene solo gli atti in vigore in quel giorno. | | `classe` | \`ClasseProvvedimento | int | None\` | | `emanazione` | \`Intervallo | None\` | intervallo di emanazione, come coppia (dal, al). | | `pubblicazione` | \`Intervallo | None\` | intervallo di pubblicazione in Gazzetta. | Restituisce: | Tipo | Descrizione | | ------------- | ------------------------------------------------------------ | | `AsyncExport` | L'esportazione appena avviata, da attendere e poi scaricare. | Solleva: | Tipo | Descrizione | | --------------------- | --------------------------------------------------------------------------------- | | `TooManyResultsError` | i criteri selezionano più atti di massimo_atti. L'esportazione non viene avviata. | | `ConnectionError` | il conteggio preventivo non è riuscito. Il messaggio indica come procedere senza. | Codice sorgente in `src/normattiva/client.py` ```python async def start_export( self, *, format: Format | str = Format.JSON, mode: ExportMode | str = ExportMode.MULTIVIGENTE, massimo_atti: int | None = 100, escludi_testo: str | None = None, escludi_titolo: str | None = None, denominazione: str | None = None, anno: int | None = None, numero: int | str | None = None, giorno: int | None = None, mese: int | None = None, titolo: str | None = None, testo: str | None = None, vigente_al: date | None = None, classe: ClasseProvvedimento | int | None = None, emanazione: Intervallo | None = None, pubblicazione: Intervallo | None = None, ) -> AsyncExport: """Chiede l'esportazione completa degli atti che una ricerca trova. Le coordinate sono le stesse di `ricerca_avanzata`. Prima di avviare l'esportazione gli atti vengono contati con una ricerca sincrona, perché un'esportazione costa minuti di lavoro al servizio e, una volta partita, non si annulla. Con `massimo_atti=None` si parte senza conteggio. `escludi_testo` ed `escludi_titolo` escludono dal risultato gli atti che contengono quelle parole, e sono l'unico filtro che l'esportazione supporta e la ricerca no: il conteggio preventivo non ne tiene conto, quindi può contare più atti di quanti ne arriveranno. Args: format: in che formato produrre l'archivio. Solo `JSON` viene poi letto in modelli. mode: quante versioni includere nell'archivio. massimo_atti: il tetto oltre il quale l'esportazione non parte. `None` la avvia senza conteggio preventivo. escludi_testo: esclude gli atti che contengono questa parola. escludi_titolo: esclude gli atti il cui titolo la contiene. denominazione: come in `ricerca_avanzata`, e così tutti i criteri che seguono. anno: anno di emanazione. numero: numero del provvedimento. giorno: giorno di emanazione. mese: mese di emanazione. titolo: parole da cercare nel titolo. testo: parole da cercare nel testo. vigente_al: tiene solo gli atti in vigore in quel giorno. classe: la classe redazionale dell'atto (senza aggiornamenti, aggiornato, abrogato). emanazione: intervallo di emanazione, come coppia `(dal, al)`. pubblicazione: intervallo di pubblicazione in Gazzetta. Returns: L'esportazione appena avviata, da attendere e poi scaricare. Raises: TooManyResultsError: i criteri selezionano più atti di `massimo_atti`. L'esportazione non viene avviata. ConnectionError: il conteggio preventivo non è riuscito. Il messaggio indica come procedere senza. """ coordinate = _coordinate( denominazione, anno, numero, giorno, mese, titolo, testo, vigente_al, classe, emanazione, pubblicazione, ) corpo = _corpo_export( coordinate, format, mode, {"testoNot": escludi_testo, "titoloNot": escludi_titolo}, ) if massimo_atti is not None: try: esito = await self.ricerca_avanzata(**coordinate, per_pagina=1) except (ConnectionError, UnexpectedResponseError) as errore: raise _conteggio_non_riuscito(errore) from errore if esito.totale > massimo_atti: raise TooManyResultsError(esito.totale, massimo_atti) risposta = await self._trasporto.post( "ricerca-asincrona/nuova-ricerca", corpo, attesi=(200, 202) ) token = risposta.testo await self._trasporto.put( "ricerca-asincrona/conferma-ricerca", {"token": token}, attesi=(200, 202, 204) ) return AsyncExport( token, self._trasporto, format=Format(format), sleep=self._sleep, clock=self._clock, ) ``` #### export_from_token ```python export_from_token( token: str, *, format: Format | str = JSON ) -> AsyncExport ``` Riprende un'esportazione già in corso, dal suo token. Codice sorgente in `src/normattiva/client.py` ```python async def export_from_token( self, token: str, *, format: Format | str = Format.JSON ) -> AsyncExport: """Riprende un'esportazione già in corso, dal suo token.""" return await AsyncExport.from_token(token, self._trasporto, format=Format(format)) ``` # Il client sincrono `Normattiva` è la classe da cui passa tutto: tiene aperta la connessione HTTP verso l'API, il limitatore di richieste e la politica dei retry, ed espone un metodo per ogni endpoint. Si costruisce una volta e si riusa, meglio se dentro un `with`, che la chiude a fine blocco. ```python from normattiva import Normattiva with Normattiva() as normattiva: atto = normattiva.dettaglio("urn:nir:stato:legge:1990-08-07;241~art2") ``` ```mermaid stateDiagram-v2 direction LR [*] --> Aperto : Normattiva(...) Aperto --> Aperto : dettaglio(), ricerca(), start_export(), ... Aperto --> Chiuso : close(), o uscita dal blocco with Chiuso --> [*] note right of Chiuso closed vale True. Un http_client passato da fuori non viene chiuso: lo chiude chi lo ha aperto. end note ``` Per l'uso asincrono c'è [`AsyncNormattiva`](https://normattiva-sdk.ireneburresi.dev/riferimento/client-asincrono/index.md), che rispecchia questa classe metodo per metodo e firma per firma. ### Normattiva ```python Normattiva( *, user_agent: str | None = None, timeout: float = 30.0, retries: int = 2, requests_per_second: float = 2.0, base_url: str = PRODUZIONE, http_client: Client | None = None, sleep: Callable[[float], None] = sleep, clock: Callable[[], float] = monotonic, ) ``` Il client sincrono verso il servizio open data di Normattiva. Tiene il pool di connessioni HTTP, l'autolimitazione delle richieste e la politica dei tentativi, ed espone un metodo per ogni endpoint dell'API. Va costruito una volta e riusato: l'autolimitazione conta le richieste di un client, quindi con un client per chiamata non limita più niente. I metodi che costano una richiesta restituiscono un modello; quelli che possono costarne molte sono iteratori, e il nome lo indica. Ogni metodo che tocca la rete può sollevare `ConnectionError` se il servizio non è raggiungibile, `UnexpectedResponseError` se la risposta non ha la forma che la libreria sa leggere, e `RequestBlockedError` se lo strato di protezione respinge la forma della richiesta. Le sezioni `Raises` dei singoli metodi elencano solo le eccezioni specifiche di ciascuno. Parametri: | Nome | Tipo | Descrizione | Predefinito | | --------------------- | ------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `user_agent` | \`str | None\` | come il client si presenta al servizio. Il predefinito nomina la libreria e il suo repository; indicare il proprio servizio e un recapito è una cortesia verso chi riceve il traffico. | | `timeout` | `float` | quanti secondi attendere una singola risposta. Le esportazioni lente vogliono un valore più alto. | `30.0` | | `retries` | `int` | quanti ritentativi dopo il primo tentativo, per ogni richiesta. Con 0 non si ritenta; valori negativi valgono 0. | `2` | | `requests_per_second` | `float` | il tetto che il client si impone. Il servizio non pubblica quote: due al secondo è una scelta prudente di questa libreria, non un limite imposto da Normattiva. Con 0 non limita. | `2.0` | | `base_url` | `str` | la radice dell'API. Si cambia per puntare a un doppio del servizio nei test. | `PRODUZIONE` | | `http_client` | \`Client | None\` | un client httpx già configurato, per metriche, tracing o intestazioni aggiuntive. Un client passato da fuori non viene chiuso da close. | | `sleep` | `Callable[[float], None]` | la funzione che attende fra un tentativo e l'altro; nei test la si sostituisce per non attendere davvero. | `sleep` | | `clock` | `Callable[[], float]` | la sorgente di tempo dell'autolimitazione, sostituibile per lo stesso motivo. | `monotonic` | Esempi: ```pycon >>> with Normattiva() as normattiva: ... atto = normattiva.dettaglio("urn:nir:stato:legge:1990-08-07;241~art2") ``` Codice sorgente in `src/normattiva/client.py` ```python def __init__( self, *, user_agent: str | None = None, timeout: float = 30.0, retries: int = 2, requests_per_second: float = 2.0, base_url: str = PRODUZIONE, http_client: httpx.Client | None = None, sleep: Callable[[float], None] = time.sleep, clock: Callable[[], float] = time.monotonic, ) -> None: self._trasporto = Trasporto( base_url=base_url, user_agent=user_agent, timeout=timeout, retries=retries, requests_per_second=requests_per_second, http_client=http_client, sleep=sleep, clock=clock, ) self._sleep = sleep self._clock = clock self._dizionari: dict[str, tuple[Tipologica, ...]] = {} ``` #### base_url ```python base_url: str ``` L'indirizzo base del servizio a cui questo client si rivolge. #### closed ```python closed: bool ``` Se questo client è stato chiuso. #### close ```python close() -> None ``` Rilascia il pool di connessioni, se è stato creato da questo client. Un client HTTP passato dall'esterno resta aperto: chiuderlo spetta a chi lo ha creato. Codice sorgente in `src/normattiva/client.py` ```python def close(self) -> None: """Rilascia il pool di connessioni, se è stato creato da questo client. Un client HTTP passato dall'esterno resta aperto: chiuderlo spetta a chi lo ha creato. """ self._trasporto.close() ``` #### dettaglio ```python dettaglio( atto: Urn | str | AttoTrovato, *, vigenza: Vigenza = None, se_troncato: SeTroncato = "segnala", ) -> DettaglioAtto ``` Legge il testo di un atto o di un suo articolo. `atto` è un URN, oppure un `AttoTrovato` uscito da una ricerca. Per i dodici tipi di atto su trenta la cui forma URN non è verificata la libreria passa dalle coordinate di Gazzetta, che il servizio accetta allo stesso modo. Quel percorso però non supporta le date: chiedere una `vigenza` per un atto raggiungibile solo così solleva un errore, il parametro non viene ignorato in silenzio. Senza `vigenza` il servizio restituisce il testo vigente oggi, e nella risposta nulla dichiara a quale data corrisponde: indicarla è l'unico modo di saperlo. Quando si indica una data, la finestra restituita viene verificata contro di essa. Con `se_troncato="solleva"` un articolo che sembra tagliato solleva `TruncationError` invece di limitarsi a segnalarlo in una proprietà. Parametri: | Nome | Tipo | Descrizione | Predefinito | | ------------- | ------------ | ---------------------------------------------------------------------------------------------------------- | ------------- | | `atto` | \`Urn | str | AttoTrovato\` | | `vigenza` | `Vigenza` | il giorno a cui leggere il testo, "originale" per la prima pubblicazione, None per il testo di oggi. | `None` | | `se_troncato` | `SeTroncato` | "segnala" registra il sospetto nella proprietà possibile_troncamento, "solleva" lo trasforma in eccezione. | `'segnala'` | Restituisce: | Tipo | Descrizione | | --------------- | --------------------------------------------------------------- | | `DettaglioAtto` | Il testo richiesto, con la finestra di vigenza in cui è valido. | Esempi: Il testo dell'articolo 19 della legge 241 com'era nel 2000, con la finestra in cui quel testo è stato in vigore:: ```text atto = normattiva.dettaglio( "urn:nir:stato:legge:1990-08-07;241~art19", vigenza=date(2000, 1, 1), ) print(atto.finestra) # 1994-01-01 → 2005-03-07 ``` Solleva: | Tipo | Descrizione | | ----------------------- | -------------------------------------------------------------------------------------------------------------- | | `NotFoundError` | nessun atto risponde a quelle coordinate. | | `AmbiguityError` | l'URN corrisponde a più atti pubblicati; i candidati sono elencati nell'eccezione. | | `NotYetInForceError` | l'articolo non esisteva alla data richiesta. | | `ValidityMismatchError` | il servizio ha risposto con una versione che non copre la data richiesta. | | `TruncationError` | solo con se_troncato="solleva", e solo se il testo sembra tagliato. | | `InvalidUrnError` | l'URN è malformato, oppure la forma URN di quel tipo di atto non è verificata. | | `InvalidArgumentError` | la vigenza è indicata due volte, oppure è chiesta per un atto raggiungibile solo dalle coordinate di Gazzetta. | Codice sorgente in `src/normattiva/client.py` ```python def dettaglio( self, atto: Urn | str | AttoTrovato, *, vigenza: Vigenza = None, se_troncato: SeTroncato = "segnala", ) -> DettaglioAtto: """Legge il testo di un atto o di un suo articolo. `atto` è un URN, oppure un `AttoTrovato` uscito da una ricerca. Per i dodici tipi di atto su trenta la cui forma URN non è verificata la libreria passa dalle coordinate di Gazzetta, che il servizio accetta allo stesso modo. Quel percorso però non supporta le date: chiedere una `vigenza` per un atto raggiungibile solo così solleva un errore, il parametro non viene ignorato in silenzio. Senza `vigenza` il servizio restituisce il testo vigente oggi, e nella risposta nulla dichiara a quale data corrisponde: indicarla è l'unico modo di saperlo. Quando si indica una data, la finestra restituita viene verificata contro di essa. Con `se_troncato="solleva"` un articolo che sembra tagliato solleva `TruncationError` invece di limitarsi a segnalarlo in una proprietà. Args: atto: un URN, la sua forma testuale, oppure un `AttoTrovato` uscito da una ricerca. Il comma viene rimosso automaticamente, perché il servizio lo rifiuta in ingresso. vigenza: il giorno a cui leggere il testo, `"originale"` per la prima pubblicazione, `None` per il testo di oggi. se_troncato: `"segnala"` registra il sospetto nella proprietà `possibile_troncamento`, `"solleva"` lo trasforma in eccezione. Returns: Il testo richiesto, con la finestra di vigenza in cui è valido. Examples: Il testo dell'articolo 19 della legge 241 com'era nel 2000, con la finestra in cui quel testo è stato in vigore:: atto = normattiva.dettaglio( "urn:nir:stato:legge:1990-08-07;241~art19", vigenza=date(2000, 1, 1), ) print(atto.finestra) # 1994-01-01 → 2005-03-07 Raises: NotFoundError: nessun atto risponde a quelle coordinate. AmbiguityError: l'URN corrisponde a più atti pubblicati; i candidati sono elencati nell'eccezione. NotYetInForceError: l'articolo non esisteva alla data richiesta. ValidityMismatchError: il servizio ha risposto con una versione che non copre la data richiesta. TruncationError: solo con `se_troncato="solleva"`, e solo se il testo sembra tagliato. InvalidUrnError: l'URN è malformato, oppure la forma URN di quel tipo di atto non è verificata. InvalidArgumentError: la vigenza è indicata due volte, oppure è chiesta per un atto raggiungibile solo dalle coordinate di Gazzetta. """ _verifica_se_troncato(se_troncato) if isinstance(atto, AttoTrovato) and not atto.ha_urn: _verifica_senza_vigenza(atto, vigenza) codice, giorno = _coordinate_di_gazzetta(atto) return self.dettaglio_da_gazzetta(codice, giorno, se_troncato=se_troncato) urn = atto.urn if isinstance(atto, AttoTrovato) else atto risposta = self._trasporto.post( "atto/dettaglio-atto-urn", {"urn": str(_urn_con_vigenza(urn, vigenza))} ) return _controlla_dettaglio(_wire.leggi_dettaglio(risposta.json()), vigenza, se_troncato) ``` #### dettaglio_da_gazzetta ```python dettaglio_da_gazzetta( codice_redazionale: str, data: date, *, se_troncato: SeTroncato = "segnala", ) -> DettaglioAtto ``` Legge un atto a partire dalle sue coordinate di Gazzetta. È il percorso per gli atti la cui forma URN non è verificata: il codice redazionale e la data di Gazzetta arrivano da una ricerca, e il servizio risponde anche per un decreto-legge luogotenenziale del 1917. Questo percorso però non supporta la multivigenza: restituisce sempre il testo vigente oggi, e per una data serve un URN. Parametri: | Nome | Tipo | Descrizione | Predefinito | | -------------------- | ------------ | ----------------------------------------------------------------------------------------------- | -------------- | | `codice_redazionale` | `str` | l'identificativo di Gazzetta dell'atto, come arriva da AttoTrovato.gazzetta.codice_redazionale. | *obbligatorio* | | `data` | `date` | la data di pubblicazione in Gazzetta. | *obbligatorio* | | `se_troncato` | `SeTroncato` | come per dettaglio. | `'segnala'` | Restituisce: | Tipo | Descrizione | | --------------- | ------------------------------------------------------- | | `DettaglioAtto` | Il testo dell'atto, sempre nella versione vigente oggi. | Solleva: | Tipo | Descrizione | | ---------------------- | ---------------------------------------------- | | `NotFoundError` | nessun atto per quelle coordinate di Gazzetta. | | `InvalidArgumentError` | il codice redazionale è vuoto. | Codice sorgente in `src/normattiva/client.py` ```python def dettaglio_da_gazzetta( self, codice_redazionale: str, data: date, *, se_troncato: SeTroncato = "segnala", ) -> DettaglioAtto: """Legge un atto a partire dalle sue coordinate di Gazzetta. È il percorso per gli atti la cui forma URN non è verificata: il codice redazionale e la data di Gazzetta arrivano da una ricerca, e il servizio risponde anche per un decreto-legge luogotenenziale del 1917. Questo percorso però non supporta la multivigenza: restituisce sempre il testo vigente oggi, e per una data serve un URN. Args: codice_redazionale: l'identificativo di Gazzetta dell'atto, come arriva da `AttoTrovato.gazzetta.codice_redazionale`. data: la data di pubblicazione in Gazzetta. se_troncato: come per `dettaglio`. Returns: Il testo dell'atto, sempre nella versione vigente oggi. Raises: NotFoundError: nessun atto per quelle coordinate di Gazzetta. InvalidArgumentError: il codice redazionale è vuoto. """ _verifica_se_troncato(se_troncato) risposta = self._trasporto.post( "atto/dettaglio-atto", _corpo_gazzetta(codice_redazionale, data) ) return _controlla_dettaglio(_wire.leggi_dettaglio(risposta.json()), None, se_troncato) ``` #### cronologia ```python cronologia( urn: Urn | str, *, massimo: int | None = None ) -> Iterator[DettaglioAtto] ``` Percorre tutte le versioni di un articolo, dall'originale a quella in vigore. Costa una richiesta per versione: le finestre di vigenza sono contigue, quindi ogni versione viene chiesta al giorno successivo alla fine della precedente. Con `massimo` l'iterazione si ferma dopo quel numero di versioni; senza, arriva all'ultima. Senza `massimo` la catena si ferma comunque dopo cinquecento passi: nessun articolo italiano ha cinquecento versioni, quindi una catena così lunga indica finestre non contigue. Parametri: | Nome | Tipo | Descrizione | Predefinito | | --------- | ----- | ----------- | ----------------------------------------------------------- | | `urn` | \`Urn | str\` | l'articolo di cui percorrere le versioni. | | `massimo` | \`int | None\` | quante versioni al più produrre. Senza, si arriva in fondo. | Produce: | Tipo | Descrizione | | --------------- | ----------------------------------------------------------- | | `DettaglioAtto` | Una versione per volta, dalla più vecchia alla più recente. | Solleva: | Tipo | Descrizione | | ------------------------- | ----------------------------------------------------------------------------------------------- | | `UnexpectedResponseError` | la catena non si chiude entro cinquecento passi, cioè le finestre di vigenza non sono contigue. | Codice sorgente in `src/normattiva/client.py` ```python def cronologia(self, urn: Urn | str, *, massimo: int | None = None) -> Iterator[DettaglioAtto]: """Percorre tutte le versioni di un articolo, dall'originale a quella in vigore. Costa una richiesta per versione: le finestre di vigenza sono contigue, quindi ogni versione viene chiesta al giorno successivo alla fine della precedente. Con `massimo` l'iterazione si ferma dopo quel numero di versioni; senza, arriva all'ultima. Senza `massimo` la catena si ferma comunque dopo cinquecento passi: nessun articolo italiano ha cinquecento versioni, quindi una catena così lunga indica finestre non contigue. Args: urn: l'articolo di cui percorrere le versioni. massimo: quante versioni al più produrre. Senza, si arriva in fondo. Yields: Una versione per volta, dalla più vecchia alla più recente. Raises: UnexpectedResponseError: la catena non si chiude entro cinquecento passi, cioè le finestre di vigenza non sono contigue. """ base = Urn.parse(urn).senza_comma prossima: Vigenza = "originale" prodotte = 0 while massimo is None or prodotte < massimo: if massimo is None and prodotte >= PASSI_MASSIMI: raise UnexpectedResponseError( f"la catena delle versioni non si chiude dopo {PASSI_MASSIMI} passi: " "le finestre di vigenza non sono contigue" ) atto = self.dettaglio(base.con_vigenza(prossima)) yield atto prodotte += 1 chiusura = atto.finestra.fine if atto.finestra else None if chiusura is None: return prossima = chiusura + timedelta(days=1) ``` #### ricerca ```python ricerca( testo: str, *, pagina: int = 1, per_pagina: int = 20, sort: Sort | str = NEWEST, tipo: str | None = None, anno: int | None = None, emettitore: str | None = None, ) -> EsitoRicerca ``` Cerca nel testo pieno del corpus. Le parole vengono combinate in AND dal servizio; non c'è modo di chiedere un OR. `tipo`, `anno` ed `emettitore` sono le faccette che la risposta stessa propone, non le coordinate dell'atto: per quelle c'è `ricerca_avanzata`. Parametri: | Nome | Tipo | Descrizione | Predefinito | | ------------ | ------ | ---------------------------------------------------- | --------------------------------------------------- | | `testo` | `str` | le parole da cercare, combinate in AND dal servizio. | *obbligatorio* | | `pagina` | `int` | quale pagina di risultati, a partire da 1. | `1` | | `per_pagina` | `int` | quanti risultati per pagina. | `20` | | `sort` | \`Sort | str\` | "newest" dal più recente, "oldest" dal più vecchio. | | `tipo` | \`str | None\` | codice della faccetta per tipo di atto, come "PLE". | | `anno` | \`int | None\` | faccetta per anno di provvedimento. | | `emettitore` | \`str | None\` | faccetta per amministrazione emanante. | Restituisce: | Tipo | Descrizione | | -------------- | --------------------------------------------------------------------- | | `EsitoRicerca` | Una pagina di risultati, con il totale e le faccette per restringere. | Codice sorgente in `src/normattiva/client.py` ```python def ricerca( self, testo: str, *, pagina: int = 1, per_pagina: int = 20, sort: Sort | str = Sort.NEWEST, tipo: str | None = None, anno: int | None = None, emettitore: str | None = None, ) -> EsitoRicerca: """Cerca nel testo pieno del corpus. Le parole vengono combinate in AND dal servizio; non c'è modo di chiedere un OR. `tipo`, `anno` ed `emettitore` sono le faccette che la risposta stessa propone, non le coordinate dell'atto: per quelle c'è `ricerca_avanzata`. Args: testo: le parole da cercare, combinate in AND dal servizio. pagina: quale pagina di risultati, a partire da 1. per_pagina: quanti risultati per pagina. sort: `"newest"` dal più recente, `"oldest"` dal più vecchio. tipo: codice della faccetta per tipo di atto, come `"PLE"`. anno: faccetta per anno di provvedimento. emettitore: faccetta per amministrazione emanante. Returns: Una pagina di risultati, con il totale e le faccette per restringere. """ corpo = _corpo_ricerca( testo, pagina=pagina, per_pagina=per_pagina, sort=sort, tipo=tipo, anno=anno, emettitore=emettitore, ) return _wire.leggi_ricerca(self._trasporto.post("ricerca/semplice", corpo).json()) ``` #### ricerca_avanzata ```python ricerca_avanzata( *, denominazione: str | None = None, anno: int | None = None, numero: int | str | None = None, giorno: int | None = None, mese: int | None = None, titolo: str | None = None, testo: str | None = None, vigente_al: date | None = None, classe: ClasseProvvedimento | int | None = None, emanazione: Intervallo | None = None, pubblicazione: Intervallo | None = None, sort: Sort | str = NEWEST, tipo: str | None = None, emettitore: str | None = None, pagina: int = 1, per_pagina: int = 20, ) -> EsitoRicerca ``` Cerca per coordinate invece che per parole. Senza nessuna coordinata il servizio risponde con l'intero corpus: è una richiesta ammessa, e la libreria non la rifiuta. Parametri: | Nome | Tipo | Descrizione | Predefinito | | --------------- | --------------------- | ------------------------------------------ | ------------------------------------------------------------------------------------------------------------ | | `denominazione` | \`str | None\` | il tipo di atto come lo scrive il dizionario, per esempio "LEGGE" o "DECRETO LEGISLATIVO". | | `anno` | \`int | None\` | anno di emanazione. | | `numero` | \`int | str | None\` | | `giorno` | \`int | None\` | giorno di emanazione. | | `mese` | \`int | None\` | mese di emanazione. | | `titolo` | \`str | None\` | parole da cercare nel titolo. | | `testo` | \`str | None\` | parole da cercare nel testo. | | `vigente_al` | \`date | None\` | tiene solo gli atti in vigore in quel giorno. | | `classe` | \`ClasseProvvedimento | int | None\` | | `emanazione` | \`Intervallo | None\` | intervallo di emanazione, come coppia (dal, al). Un estremo può essere None per lasciare la finestra aperta. | | `pubblicazione` | \`Intervallo | None\` | intervallo di pubblicazione in Gazzetta, come sopra. | | `sort` | \`Sort | str\` | "newest" dal più recente, "oldest" dal più vecchio. | | `tipo` | \`str | None\` | faccetta per tipo di atto. | | `emettitore` | \`str | None\` | faccetta per amministrazione emanante. | | `pagina` | `int` | quale pagina di risultati, a partire da 1. | `1` | | `per_pagina` | `int` | quanti risultati per pagina. | `20` | Restituisce: | Tipo | Descrizione | | -------------- | -------------------------------------------------------------- | | `EsitoRicerca` | Una pagina di risultati, nella stessa forma che rende ricerca. | Codice sorgente in `src/normattiva/client.py` ```python def ricerca_avanzata( self, *, denominazione: str | None = None, anno: int | None = None, numero: int | str | None = None, giorno: int | None = None, mese: int | None = None, titolo: str | None = None, testo: str | None = None, vigente_al: date | None = None, classe: ClasseProvvedimento | int | None = None, emanazione: Intervallo | None = None, pubblicazione: Intervallo | None = None, sort: Sort | str = Sort.NEWEST, tipo: str | None = None, emettitore: str | None = None, pagina: int = 1, per_pagina: int = 20, ) -> EsitoRicerca: """Cerca per coordinate invece che per parole. Senza nessuna coordinata il servizio risponde con l'intero corpus: è una richiesta ammessa, e la libreria non la rifiuta. Args: denominazione: il tipo di atto come lo scrive il dizionario, per esempio `"LEGGE"` o `"DECRETO LEGISLATIVO"`. anno: anno di emanazione. numero: numero del provvedimento. giorno: giorno di emanazione. mese: mese di emanazione. titolo: parole da cercare nel titolo. testo: parole da cercare nel testo. vigente_al: tiene solo gli atti in vigore in quel giorno. classe: la classe redazionale dell'atto (senza aggiornamenti, aggiornato, abrogato). emanazione: intervallo di emanazione, come coppia `(dal, al)`. Un estremo può essere `None` per lasciare la finestra aperta. pubblicazione: intervallo di pubblicazione in Gazzetta, come sopra. sort: `"newest"` dal più recente, `"oldest"` dal più vecchio. tipo: faccetta per tipo di atto. emettitore: faccetta per amministrazione emanante. pagina: quale pagina di risultati, a partire da 1. per_pagina: quanti risultati per pagina. Returns: Una pagina di risultati, nella stessa forma che rende `ricerca`. """ corpo = _corpo_avanzata( _coordinate( denominazione, anno, numero, giorno, mese, titolo, testo, vigente_al, classe, emanazione, pubblicazione, ), sort, pagina, per_pagina, tipo, emettitore, ) return _wire.leggi_ricerca(self._trasporto.post("ricerca/avanzata", corpo).json()) ``` #### ricerca_completa ```python ricerca_completa( testo: str, *, massimo: int | None = None, per_pagina: int = 50, sort: Sort | str = NEWEST, tipo: str | None = None, anno: int | None = None, emettitore: str | None = None, ) -> Iterator[AttoTrovato] ``` Scorre tutti gli atti che una ricerca trova, una pagina per volta. L'iteratore è pigro: ogni pagina viene chiesta solo quando serve, quindi consumare dieci risultati costa una richiesta sola. Con `massimo` ci si ferma dopo quel numero di atti; per sapere quanti ce n'erano in tutto basta una `ricerca` e il suo `totale`. Quel `totale` è anche la condizione di uscita: il numero di pagina restituito dal servizio non è affidabile, e usarlo come condizione potrebbe rileggere la prima pagina all'infinito. Parametri: | Nome | Tipo | Descrizione | Predefinito | | ------------ | ------ | ---------------------------------------- | ------------------------------------------------------- | | `testo` | `str` | le parole da cercare. | *obbligatorio* | | `massimo` | \`int | None\` | quanti atti al più produrre. Senza, si arriva in fondo. | | `per_pagina` | `int` | quanti risultati chiedere per richiesta. | `50` | | `sort` | \`Sort | str\` | "newest" dal più recente, "oldest" dal più vecchio. | | `tipo` | \`str | None\` | faccetta per tipo di atto. | | `anno` | \`int | None\` | faccetta per anno di provvedimento. | | `emettitore` | \`str | None\` | faccetta per amministrazione emanante. | Produce: | Tipo | Descrizione | | ------------- | ----------------------------------------------------------- | | `AttoTrovato` | Un atto per volta, nell'ordine in cui il servizio li rende. | Codice sorgente in `src/normattiva/client.py` ```python def ricerca_completa( self, testo: str, *, massimo: int | None = None, per_pagina: int = 50, sort: Sort | str = Sort.NEWEST, tipo: str | None = None, anno: int | None = None, emettitore: str | None = None, ) -> Iterator[AttoTrovato]: """Scorre tutti gli atti che una ricerca trova, una pagina per volta. L'iteratore è pigro: ogni pagina viene chiesta solo quando serve, quindi consumare dieci risultati costa una richiesta sola. Con `massimo` ci si ferma dopo quel numero di atti; per sapere quanti ce n'erano in tutto basta una `ricerca` e il suo `totale`. Quel `totale` è anche la condizione di uscita: il numero di pagina restituito dal servizio non è affidabile, e usarlo come condizione potrebbe rileggere la prima pagina all'infinito. Args: testo: le parole da cercare. massimo: quanti atti al più produrre. Senza, si arriva in fondo. per_pagina: quanti risultati chiedere per richiesta. sort: `"newest"` dal più recente, `"oldest"` dal più vecchio. tipo: faccetta per tipo di atto. anno: faccetta per anno di provvedimento. emettitore: faccetta per amministrazione emanante. Yields: Un atto per volta, nell'ordine in cui il servizio li rende. """ if massimo is not None and massimo <= 0: return prodotti = 0 pagina = 1 dichiarati: int | None = None quanti_per_pagina = per_pagina if massimo is None else min(per_pagina, massimo) while True: esito = self.ricerca( testo, pagina=pagina, per_pagina=quanti_per_pagina, sort=sort, tipo=tipo, anno=anno, emettitore=emettitore, ) if dichiarati is None: dichiarati = esito.totale for atto in esito.atti: yield atto prodotti += 1 if massimo is not None and prodotti >= massimo: return if not esito.atti or prodotti >= dichiarati or pagina >= PAGINE_MASSIME: return pagina += 1 ``` #### atti_aggiornati ```python atti_aggiornati( dal: date, al: date ) -> Iterator[AttoTrovato] ``` Elenca gli atti modificati fra due date. Il flusso contiene solo le modifiche: un atto pubblicato dentro la finestra ma mai modificato dopo non compare. Le finestre più lunghe di un anno vengono spezzate, perché il servizio le rifiuta. Parametri: | Nome | Tipo | Descrizione | Predefinito | | ----- | ------ | --------------------------------------- | -------------- | | `dal` | `date` | primo giorno della finestra, compreso. | *obbligatorio* | | `al` | `date` | ultimo giorno della finestra, compreso. | *obbligatorio* | Produce: | Tipo | Descrizione | | ------------- | ----------------------------------------------------- | | `AttoTrovato` | Un atto modificato per volta, finestra dopo finestra. | Solleva: | Tipo | Descrizione | | -------------------- | --------------------------------------------------------------------------------------- | | `RuleViolationError` | al precede dal. Sollevata prima di toccare la rete, col codice RuleCode.DATE_INVERTITE. | Codice sorgente in `src/normattiva/client.py` ```python def atti_aggiornati(self, dal: date, al: date) -> Iterator[AttoTrovato]: """Elenca gli atti modificati fra due date. Il flusso contiene solo le modifiche: un atto pubblicato dentro la finestra ma mai modificato dopo non compare. Le finestre più lunghe di un anno vengono spezzate, perché il servizio le rifiuta. Args: dal: primo giorno della finestra, compreso. al: ultimo giorno della finestra, compreso. Yields: Un atto modificato per volta, finestra dopo finestra. Raises: RuleViolationError: `al` precede `dal`. Sollevata prima di toccare la rete, col codice `RuleCode.DATE_INVERTITE`. """ _verifica_intervallo(dal, al) for inizio, fine in _intervalli(dal, al): corpo = _corpo_aggiornati(inizio, fine) esito = _wire.leggi_ricerca(self._trasporto.post("ricerca/aggiornati", corpo).json()) yield from esito.atti ``` #### denominazioni ```python denominazioni( *, reload: bool = False ) -> tuple[Tipologica, ...] ``` Elenca i tipi di atto che il corpus contiene, e tiene il risultato in memoria. Codice sorgente in `src/normattiva/client.py` ```python def denominazioni(self, *, reload: bool = False) -> tuple[Tipologica, ...]: """Elenca i tipi di atto che il corpus contiene, e tiene il risultato in memoria.""" return self._dizionario( "denominazioni", "tipologiche/denominazione-atto", _wire.leggi_denominazioni, reload ) ``` #### classi_provvedimento ```python classi_provvedimento( *, reload: bool = False ) -> tuple[Tipologica, ...] ``` Elenca le classi redazionali a cui un atto può appartenere. Codice sorgente in `src/normattiva/client.py` ```python def classi_provvedimento(self, *, reload: bool = False) -> tuple[Tipologica, ...]: """Elenca le classi redazionali a cui un atto può appartenere.""" return self._dizionario( "classi", "tipologiche/classe-provvedimento", _wire.leggi_classi, reload ) ``` #### export_formats ```python export_formats( *, reload: bool = False ) -> tuple[Tipologica, ...] ``` Elenca i formati in cui si può chiedere un'esportazione. Codice sorgente in `src/normattiva/client.py` ```python def export_formats(self, *, reload: bool = False) -> tuple[Tipologica, ...]: """Elenca i formati in cui si può chiedere un'esportazione.""" return self._dizionario( "estensioni", "tipologiche/estensioni", _wire.leggi_estensioni, reload ) ``` #### ricerche_predefinite ```python ricerche_predefinite() -> tuple[RicercaPredefinita, ...] ``` Elenca le ricerche predefinite che il servizio propone. Codice sorgente in `src/normattiva/client.py` ```python def ricerche_predefinite(self) -> tuple[RicercaPredefinita, ...]: """Elenca le ricerche predefinite che il servizio propone.""" return _wire.leggi_ricerche_predefinite(self._trasporto.get("ricerca/predefinita").json()) ``` #### collections ```python collections() -> tuple[Collection, ...] ``` Elenca gli archivi già confezionati che il servizio mette a disposizione. Codice sorgente in `src/normattiva/client.py` ```python def collections(self) -> tuple[Collection, ...]: """Elenca gli archivi già confezionati che il servizio mette a disposizione.""" return _wire.leggi_collezioni( self._trasporto.get("collections/collection-predefinite").json() ) ``` #### download_collection ```python download_collection( name: str, *, format: Format | str = JSON, mode: ExportMode | str = VIGENTE, ) -> Corpus ``` Scarica un archivio già confezionato e legge gli atti che contiene. Solleva: | Tipo | Descrizione | | ---------------------- | ------------------------------------------------------------------------------------------ | | `InvalidArgumentError` | il formato chiesto non viene letto in modelli; usare save_collection per averlo come file. | Codice sorgente in `src/normattiva/client.py` ```python def download_collection( self, name: str, *, format: Format | str = Format.JSON, mode: ExportMode | str = ExportMode.VIGENTE, ) -> Corpus: """Scarica un archivio già confezionato e legge gli atti che contiene. Raises: InvalidArgumentError: il formato chiesto non viene letto in modelli; usare `save_collection` per averlo come file. """ _verifica_leggibile(Format(format), "save_collection()") return Corpus.from_data(self._archivio_collezione(name, format, mode)) ``` #### save_collection ```python save_collection( name: str, path: str | Path, *, format: Format | str = JSON, mode: ExportMode | str = VIGENTE, ) -> Path ``` Scarica su disco un archivio già confezionato, in qualunque formato sia. Codice sorgente in `src/normattiva/client.py` ```python def save_collection( self, name: str, path: str | Path, *, format: Format | str = Format.JSON, mode: ExportMode | str = ExportMode.VIGENTE, ) -> Path: """Scarica su disco un archivio già confezionato, in qualunque formato sia.""" destinazione = Path(path) destinazione.write_bytes(self._archivio_collezione(name, format, mode)) return destinazione ``` #### start_export ```python start_export( *, format: Format | str = JSON, mode: ExportMode | str = MULTIVIGENTE, massimo_atti: int | None = 100, escludi_testo: str | None = None, escludi_titolo: str | None = None, denominazione: str | None = None, anno: int | None = None, numero: int | str | None = None, giorno: int | None = None, mese: int | None = None, titolo: str | None = None, testo: str | None = None, vigente_al: date | None = None, classe: ClasseProvvedimento | int | None = None, emanazione: Intervallo | None = None, pubblicazione: Intervallo | None = None, ) -> Export ``` Chiede l'esportazione completa degli atti che una ricerca trova. Le coordinate sono le stesse di `ricerca_avanzata`. Prima di avviare l'esportazione gli atti vengono contati con una ricerca sincrona, perché un'esportazione costa minuti di lavoro al servizio e, una volta partita, non si annulla. Con `massimo_atti=None` si parte senza conteggio. `escludi_testo` ed `escludi_titolo` escludono dal risultato gli atti che contengono quelle parole, e sono l'unico filtro che l'esportazione supporta e la ricerca no: il conteggio preventivo non ne tiene conto, quindi può contare più atti di quanti ne arriveranno. Parametri: | Nome | Tipo | Descrizione | Predefinito | | ---------------- | --------------------- | ----------- | ------------------------------------------------------------------------------------------- | | `format` | \`Format | str\` | in che formato produrre l'archivio. Solo JSON viene poi letto in modelli. | | `mode` | \`ExportMode | str\` | quante versioni includere nell'archivio. | | `massimo_atti` | \`int | None\` | il tetto oltre il quale l'esportazione non parte. None la avvia senza conteggio preventivo. | | `escludi_testo` | \`str | None\` | esclude gli atti che contengono questa parola. | | `escludi_titolo` | \`str | None\` | esclude gli atti il cui titolo la contiene. | | `denominazione` | \`str | None\` | come in ricerca_avanzata, e così tutti i criteri che seguono. | | `anno` | \`int | None\` | anno di emanazione. | | `numero` | \`int | str | None\` | | `giorno` | \`int | None\` | giorno di emanazione. | | `mese` | \`int | None\` | mese di emanazione. | | `titolo` | \`str | None\` | parole da cercare nel titolo. | | `testo` | \`str | None\` | parole da cercare nel testo. | | `vigente_al` | \`date | None\` | tiene solo gli atti in vigore in quel giorno. | | `classe` | \`ClasseProvvedimento | int | None\` | | `emanazione` | \`Intervallo | None\` | intervallo di emanazione, come coppia (dal, al). | | `pubblicazione` | \`Intervallo | None\` | intervallo di pubblicazione in Gazzetta. | Restituisce: | Tipo | Descrizione | | -------- | ------------------------------------------------------------ | | `Export` | L'esportazione appena avviata, da attendere e poi scaricare. | Solleva: | Tipo | Descrizione | | --------------------- | --------------------------------------------------------------------------------- | | `TooManyResultsError` | i criteri selezionano più atti di massimo_atti. L'esportazione non viene avviata. | | `ConnectionError` | il conteggio preventivo non è riuscito. Il messaggio indica come procedere senza. | Codice sorgente in `src/normattiva/client.py` ```python def start_export( self, *, format: Format | str = Format.JSON, mode: ExportMode | str = ExportMode.MULTIVIGENTE, massimo_atti: int | None = 100, escludi_testo: str | None = None, escludi_titolo: str | None = None, denominazione: str | None = None, anno: int | None = None, numero: int | str | None = None, giorno: int | None = None, mese: int | None = None, titolo: str | None = None, testo: str | None = None, vigente_al: date | None = None, classe: ClasseProvvedimento | int | None = None, emanazione: Intervallo | None = None, pubblicazione: Intervallo | None = None, ) -> Export: """Chiede l'esportazione completa degli atti che una ricerca trova. Le coordinate sono le stesse di `ricerca_avanzata`. Prima di avviare l'esportazione gli atti vengono contati con una ricerca sincrona, perché un'esportazione costa minuti di lavoro al servizio e, una volta partita, non si annulla. Con `massimo_atti=None` si parte senza conteggio. `escludi_testo` ed `escludi_titolo` escludono dal risultato gli atti che contengono quelle parole, e sono l'unico filtro che l'esportazione supporta e la ricerca no: il conteggio preventivo non ne tiene conto, quindi può contare più atti di quanti ne arriveranno. Args: format: in che formato produrre l'archivio. Solo `JSON` viene poi letto in modelli. mode: quante versioni includere nell'archivio. massimo_atti: il tetto oltre il quale l'esportazione non parte. `None` la avvia senza conteggio preventivo. escludi_testo: esclude gli atti che contengono questa parola. escludi_titolo: esclude gli atti il cui titolo la contiene. denominazione: come in `ricerca_avanzata`, e così tutti i criteri che seguono. anno: anno di emanazione. numero: numero del provvedimento. giorno: giorno di emanazione. mese: mese di emanazione. titolo: parole da cercare nel titolo. testo: parole da cercare nel testo. vigente_al: tiene solo gli atti in vigore in quel giorno. classe: la classe redazionale dell'atto (senza aggiornamenti, aggiornato, abrogato). emanazione: intervallo di emanazione, come coppia `(dal, al)`. pubblicazione: intervallo di pubblicazione in Gazzetta. Returns: L'esportazione appena avviata, da attendere e poi scaricare. Raises: TooManyResultsError: i criteri selezionano più atti di `massimo_atti`. L'esportazione non viene avviata. ConnectionError: il conteggio preventivo non è riuscito. Il messaggio indica come procedere senza. """ coordinate = _coordinate( denominazione, anno, numero, giorno, mese, titolo, testo, vigente_al, classe, emanazione, pubblicazione, ) corpo = _corpo_export( coordinate, format, mode, {"testoNot": escludi_testo, "titoloNot": escludi_titolo}, ) if massimo_atti is not None: try: quanti = self.ricerca_avanzata(**coordinate, per_pagina=1).totale except (ConnectionError, UnexpectedResponseError) as errore: raise _conteggio_non_riuscito(errore) from errore if quanti > massimo_atti: raise TooManyResultsError(quanti, massimo_atti) risposta = self._trasporto.post("ricerca-asincrona/nuova-ricerca", corpo, attesi=(200, 202)) token = risposta.testo self._trasporto.put( "ricerca-asincrona/conferma-ricerca", {"token": token}, attesi=(200, 202, 204) ) return Export( token, self._trasporto, format=Format(format), sleep=self._sleep, clock=self._clock, ) ``` #### export_from_token ```python export_from_token( token: str, *, format: Format | str = JSON ) -> Export ``` Riprende un'esportazione già in corso, dal suo token. Codice sorgente in `src/normattiva/client.py` ```python def export_from_token(self, token: str, *, format: Format | str = Format.JSON) -> Export: """Riprende un'esportazione già in corso, dal suo token.""" return Export.from_token(token, self._trasporto, format=Format(format)) ``` # Gli atti notissimi Ogni voce è un AttoNoto: l'atto nel suo insieme, più l'allegato attraverso cui rispondono i suoi articoli. ### AttoNoto ```python AttoNoto( nome: str, base: Urn, allegato_articoli: str | None = None, ) ``` Un atto noto, richiamabile per nome invece che per URN. #### urn ```python urn: Urn ``` L'URN dell'atto nel suo insieme. #### articolo ```python articolo(numero: str | int) -> Urn ``` Compone l'URN di un articolo, passando per l'allegato che lo contiene. Parametri: | Nome | Tipo | Descrizione | Predefinito | | -------- | ----- | ----------- | ---------------------------------------------------------------------------------- | | `numero` | \`str | int\` | il numero dell'articolo, con l'eventuale ordinale attaccato (416bis, non 416-bis). | Esempi: ```pycon >>> from normattiva.codici import CODICE_CIVILE >>> str(CODICE_CIVILE.articolo(2043)) 'urn:nir:stato:regio.decreto:1942-03-16;262:2~art2043' ``` Codice sorgente in `src/normattiva/codici.py` ```python def articolo(self, numero: str | int) -> Urn: """Compone l'URN di un articolo, passando per l'allegato che lo contiene. Args: numero: il numero dell'articolo, con l'eventuale ordinale attaccato (`416bis`, non `416-bis`). Examples: >>> from normattiva.codici import CODICE_CIVILE >>> str(CODICE_CIVILE.articolo(2043)) 'urn:nir:stato:regio.decreto:1942-03-16;262:2~art2043' """ return replace(self.base, allegato=self.allegato_articoli, articolo=str(numero)) ``` ## L'elenco | Costante | Atto | | --------------------------------- | -------------------------------- | | `COSTITUZIONE` | Costituzione della Repubblica | | `CODICE_CIVILE` | R.D. 16 marzo 1942, n. 262 | | `CODICE_PENALE` | R.D. 19 ottobre 1930, n. 1398 | | `CODICE_PROCEDURA_CIVILE` | R.D. 28 ottobre 1940, n. 1443 | | `CODICE_PROCEDURA_PENALE` | D.P.R. 22 settembre 1988, n. 447 | | `CODICE_AMMINISTRAZIONE_DIGITALE` | D.Lgs. 7 marzo 2005, n. 82 | | `CODICE_PRIVACY` | D.Lgs. 30 giugno 2003, n. 196 | | `CODICE_DELLA_STRADA` | D.Lgs. 30 aprile 1992, n. 285 | | `CODICE_DEL_CONSUMO` | D.Lgs. 6 settembre 2005, n. 206 | | `TUIR` | D.P.R. 22 dicembre 1986, n. 917 | | `TESTO_UNICO_EDILIZIA` | D.P.R. 6 giugno 2001, n. 380 | | `STATUTO_DEI_LAVORATORI` | L. 20 maggio 1970, n. 300 | Gli articoli di ciascuno rispondono attraverso l'allegato indicato da `allegato_articoli`, che `articolo()` mette nell'URN al posto tuo. Atti di uso comune, con l'allegato attraverso cui i loro articoli sono indirizzabili. Gli articoli dei codici furono approvati come allegato a un decreto e non sono indirizzabili sotto il decreto stesso; quale sia l'allegato cambia da codice a codice. Ogni corrispondenza in questo modulo è stata verificata contro il servizio il 2026-08-24. ### COSTITUZIONE ```python COSTITUZIONE = AttoNoto( "Costituzione", Urn( denominazione="costituzione", anno=1947, data=date(1947, 12, 27), ), ) ``` ### CODICE_CIVILE ```python CODICE_CIVILE = AttoNoto( "Codice civile", Urn.regio_decreto(1942, 262, data=date(1942, 3, 16)), allegato_articoli="2", ) ``` ### CODICE_PENALE ```python CODICE_PENALE = AttoNoto( "Codice penale", Urn.regio_decreto(1930, 1398, data=date(1930, 10, 19)), allegato_articoli="1", ) ``` ### CODICE_PROCEDURA_CIVILE ```python CODICE_PROCEDURA_CIVILE = AttoNoto( "Codice di procedura civile", Urn.regio_decreto(1940, 1443, data=date(1940, 10, 28)), allegato_articoli="1", ) ``` ### CODICE_PROCEDURA_PENALE ```python CODICE_PROCEDURA_PENALE = AttoNoto( "Codice di procedura penale", Urn.dpr(1988, 447, data=date(1988, 9, 22)), ) ``` ### CODICE_AMMINISTRAZIONE_DIGITALE ```python CODICE_AMMINISTRAZIONE_DIGITALE = AttoNoto( "Codice dell'amministrazione digitale", Urn.decreto_legislativo( 2005, 82, data=date(2005, 3, 7) ), ) ``` ### CODICE_PRIVACY ```python CODICE_PRIVACY = AttoNoto( "Codice in materia di protezione dei dati personali", Urn.decreto_legislativo( 2003, 196, data=date(2003, 6, 30) ), ) ``` ### CODICE_DELLA_STRADA ```python CODICE_DELLA_STRADA = AttoNoto( "Codice della strada", Urn.decreto_legislativo( 1992, 285, data=date(1992, 4, 30) ), ) ``` ### CODICE_DEL_CONSUMO ```python CODICE_DEL_CONSUMO = AttoNoto( "Codice del consumo", Urn.decreto_legislativo( 2005, 206, data=date(2005, 9, 6) ), ) ``` ### TUIR ```python TUIR = AttoNoto( "Testo unico delle imposte sui redditi", Urn.dpr(1986, 917, data=date(1986, 12, 22)), ) ``` ### TESTO_UNICO_EDILIZIA ```python TESTO_UNICO_EDILIZIA = AttoNoto( "Testo unico dell'edilizia", Urn.dpr(2001, 380, data=date(2001, 6, 6)), ) ``` ### STATUTO_DEI_LAVORATORI ```python STATUTO_DEI_LAVORATORI = AttoNoto( "Statuto dei lavoratori", Urn.legge(1970, 300, data=date(1970, 5, 20)), ) ``` ### \_TUTTI ```python _TUTTI = ( COSTITUZIONE, CODICE_CIVILE, CODICE_PENALE, CODICE_PROCEDURA_CIVILE, CODICE_PROCEDURA_PENALE, CODICE_AMMINISTRAZIONE_DIGITALE, CODICE_PRIVACY, CODICE_DELLA_STRADA, CODICE_DEL_CONSUMO, TUIR, TESTO_UNICO_EDILIZIA, STATUTO_DEI_LAVORATORI, ) ``` ### __all__ ```python __all__ = [ "CODICE_AMMINISTRAZIONE_DIGITALE", "CODICE_CIVILE", "CODICE_DELLA_STRADA", "CODICE_DEL_CONSUMO", "CODICE_PENALE", "CODICE_PRIVACY", "CODICE_PROCEDURA_CIVILE", "CODICE_PROCEDURA_PENALE", "COSTITUZIONE", "STATUTO_DEI_LAVORATORI", "TESTO_UNICO_EDILIZIA", "TUIR", "AttoNoto", "tutti", ] ``` ### Urn ```python Urn( denominazione: str, anno: int, data: date | None = None, numero: str | None = None, autorita: str = "stato", allegato: str | None = None, articolo: str | None = None, comma: str | None = None, versione: date | Literal["originale"] | None = None, ) ``` Un identificatore NIR, scomposto nelle sue parti. Il suffisso di versione fa parte dell'identificatore perché i rimandi dentro il testo restituito lo includono. Vale lo stesso per il comma, che però il servizio rifiuta in ingresso: `senza_comma` restituisce l'identificatore che si può davvero usare in una richiesta. `numero`, `allegato` e `articolo` accettano anche interi e li conservano come stringhe: `numero=300` e `numero="300"` costruiscono lo stesso URN. L'articolo viene inoltre normalizzato (`"5-bis"` non è ammesso, `"5bis"` sì). #### senza_comma ```python senza_comma: Urn ``` Lo stesso URN senza il comma, che il servizio rifiuta in ingresso. #### permalink ```python permalink: str ``` Il link pubblico di Normattiva, per verificare sulla fonte. #### parse ```python parse(testo: str | Urn) -> Urn ``` Costruisce un `Urn` dalla sua forma testuale. Parametri: | Nome | Tipo | Descrizione | Predefinito | | ------- | ----- | ----------- | ----------------------------------------------------------------------- | | `testo` | \`str | Urn\` | la forma testuale, oppure un Urn già letto, che viene restituito com'è. | Restituisce: | Tipo | Descrizione | | ----- | ------------------------------------------- | | `Urn` | L'identificatore scomposto nelle sue parti. | Esempi: ```pycon >>> from normattiva import Urn >>> Urn.parse("urn:nir:stato:legge:1990-08-07;241~art5").articolo '5' ``` Solleva: | Tipo | Descrizione | | ----------------- | ------------------------------------------------------------------------- | | `InvalidUrnError` | il testo non rispetta la grammatica NIR, o porta una data che non esiste. | Codice sorgente in `src/normattiva/urn.py` ```python @classmethod def parse(cls, testo: str | Urn) -> Urn: """Costruisce un `Urn` dalla sua forma testuale. Args: testo: la forma testuale, oppure un `Urn` già letto, che viene restituito com'è. Returns: L'identificatore scomposto nelle sue parti. Examples: >>> from normattiva import Urn >>> Urn.parse("urn:nir:stato:legge:1990-08-07;241~art5").articolo '5' Raises: InvalidUrnError: il testo non rispetta la grammatica NIR, o porta una data che non esiste. """ if isinstance(testo, Urn): return testo pezzi = _GRAMMATICA.match(str(testo).strip().lower()) if pezzi is None: raise InvalidUrnError(testo) grezza = pezzi["data"] data = _leggi_data(grezza) if len(grezza) > 4 else None vigenza = pezzi["vigenza"] versione: date | Literal["originale"] | None = None if vigenza: versione = _leggi_data(vigenza) elif pezzi["originale"]: versione = "originale" return cls( denominazione=pezzi["denominazione"], anno=data.year if data else int(grezza), data=data, numero=pezzi["numero"], autorita=pezzi["autorita"], allegato=pezzi["allegato"], articolo=pezzi["articolo"], comma=pezzi["comma"], versione=versione, ) ``` #### legge ```python legge( anno: int, numero: int | str, *, articolo: int | str | None = None, data: date | None = None, ) -> Urn ``` Costruisce l'URN di una legge. Parametri: | Nome | Tipo | Descrizione | Predefinito | | ---------- | ------ | ------------------- | ----------------------------------------------------------------------------------------------------------------------- | | `anno` | `int` | anno di emanazione. | *obbligatorio* | | `numero` | \`int | str\` | numero della legge, come intero o come stringa. | | `articolo` | \`int | str | None\` | | `data` | \`date | None\` | la data esatta di emanazione. Rende l'URN più preciso e disambigua fra due atti con lo stesso numero nello stesso anno. | Esempi: ```pycon >>> from normattiva import Urn >>> str(Urn.legge(1990, 241, articolo=5)) 'urn:nir:stato:legge:1990;241~art5' ``` Codice sorgente in `src/normattiva/urn.py` ```python @classmethod def legge( cls, anno: int, numero: int | str, *, articolo: int | str | None = None, data: date | None = None, ) -> Urn: """Costruisce l'URN di una legge. Args: anno: anno di emanazione. numero: numero della legge, come intero o come stringa. articolo: l'articolo da indirizzare, se ne serve uno solo. data: la data esatta di emanazione. Rende l'URN più preciso e disambigua fra due atti con lo stesso numero nello stesso anno. Examples: >>> from normattiva import Urn >>> str(Urn.legge(1990, 241, articolo=5)) 'urn:nir:stato:legge:1990;241~art5' """ return cls._di_tipo(LEGGE, anno, numero, articolo=articolo, data=data) ``` #### decreto_legge ```python decreto_legge( anno: int, numero: int | str, *, articolo: int | str | None = None, data: date | None = None, ) -> Urn ``` Costruisce l'URN di un decreto-legge. Codice sorgente in `src/normattiva/urn.py` ```python @classmethod def decreto_legge( cls, anno: int, numero: int | str, *, articolo: int | str | None = None, data: date | None = None, ) -> Urn: """Costruisce l'URN di un decreto-legge.""" return cls._di_tipo(DECRETO_LEGGE, anno, numero, articolo=articolo, data=data) ``` #### decreto_legislativo ```python decreto_legislativo( anno: int, numero: int | str, *, articolo: int | str | None = None, data: date | None = None, ) -> Urn ``` Costruisce l'URN di un decreto legislativo. Codice sorgente in `src/normattiva/urn.py` ```python @classmethod def decreto_legislativo( cls, anno: int, numero: int | str, *, articolo: int | str | None = None, data: date | None = None, ) -> Urn: """Costruisce l'URN di un decreto legislativo.""" return cls._di_tipo(DECRETO_LEGISLATIVO, anno, numero, articolo=articolo, data=data) ``` #### dpr ```python dpr( anno: int, numero: int | str, *, articolo: int | str | None = None, data: date | None = None, ) -> Urn ``` Costruisce l'URN di un decreto del Presidente della Repubblica. Codice sorgente in `src/normattiva/urn.py` ```python @classmethod def dpr( cls, anno: int, numero: int | str, *, articolo: int | str | None = None, data: date | None = None, ) -> Urn: """Costruisce l'URN di un decreto del Presidente della Repubblica.""" return cls._di_tipo(DPR, anno, numero, articolo=articolo, data=data) ``` #### regio_decreto ```python regio_decreto( anno: int, numero: int | str, *, articolo: int | str | None = None, data: date | None = None, ) -> Urn ``` Costruisce l'URN di un regio decreto. Codice sorgente in `src/normattiva/urn.py` ```python @classmethod def regio_decreto( cls, anno: int, numero: int | str, *, articolo: int | str | None = None, data: date | None = None, ) -> Urn: """Costruisce l'URN di un regio decreto.""" return cls._di_tipo(REGIO_DECRETO, anno, numero, articolo=articolo, data=data) ``` #### con_articolo ```python con_articolo(articolo: int | str) -> Urn ``` Costruisce lo stesso atto, indirizzato a uno dei suoi articoli. Codice sorgente in `src/normattiva/urn.py` ```python def con_articolo(self, articolo: int | str) -> Urn: """Costruisce lo stesso atto, indirizzato a uno dei suoi articoli.""" return replace(self, articolo=str(articolo), comma=None) ``` #### con_vigenza ```python con_vigenza(vigenza: date | Literal['originale']) -> Urn ``` Restituisce lo stesso URN con la data di vigenza indicata. Codice sorgente in `src/normattiva/urn.py` ```python def con_vigenza(self, vigenza: date | Literal["originale"]) -> Urn: """Restituisce lo stesso URN con la data di vigenza indicata.""" return replace(self, versione=vigenza) ``` ### AttoNoto ```python AttoNoto( nome: str, base: Urn, allegato_articoli: str | None = None, ) ``` Un atto noto, richiamabile per nome invece che per URN. #### urn ```python urn: Urn ``` L'URN dell'atto nel suo insieme. #### articolo ```python articolo(numero: str | int) -> Urn ``` Compone l'URN di un articolo, passando per l'allegato che lo contiene. Parametri: | Nome | Tipo | Descrizione | Predefinito | | -------- | ----- | ----------- | ---------------------------------------------------------------------------------- | | `numero` | \`str | int\` | il numero dell'articolo, con l'eventuale ordinale attaccato (416bis, non 416-bis). | Esempi: ```pycon >>> from normattiva.codici import CODICE_CIVILE >>> str(CODICE_CIVILE.articolo(2043)) 'urn:nir:stato:regio.decreto:1942-03-16;262:2~art2043' ``` Codice sorgente in `src/normattiva/codici.py` ```python def articolo(self, numero: str | int) -> Urn: """Compone l'URN di un articolo, passando per l'allegato che lo contiene. Args: numero: il numero dell'articolo, con l'eventuale ordinale attaccato (`416bis`, non `416-bis`). Examples: >>> from normattiva.codici import CODICE_CIVILE >>> str(CODICE_CIVILE.articolo(2043)) 'urn:nir:stato:regio.decreto:1942-03-16;262:2~art2043' """ return replace(self.base, allegato=self.allegato_articoli, articolo=str(numero)) ``` ### tutti ```python tutti() -> tuple[AttoNoto, ...] ``` Restituisce tutti gli atti noti definiti in questo modulo. Codice sorgente in `src/normattiva/codici.py` ```python def tutti() -> tuple[AttoNoto, ...]: """Restituisce tutti gli atti noti definiti in questo modulo.""" return _TUTTI ``` # Gli endpoint L'API open data di Normattiva espone quindici endpoint. La tabella indica quale metodo della libreria copre ciascuno. | Endpoint | Metodo della libreria | | --------------------------------------------------- | ------------------------- | | `atto/dettaglio-atto-urn` | dettaglio | | `atto/dettaglio-atto` | dettaglio_da_gazzetta | | `ricerca/semplice` | ricerca, ricerca_completa | | `ricerca/avanzata` | ricerca_avanzata | | `ricerca/aggiornati` | atti_aggiornati | | `ricerca/predefinita` | ricerche_predefinite | | `ricerca-asincrona/nuova-ricerca` | start_export | | `ricerca-asincrona/conferma-ricerca` | start_export | | `ricerca-asincrona/check-status/{token}` | Export.refresh | | `collections/download/collection-asincrona/{token}` | Export.download | | `collections/collection-predefinite` | collections | | `collections/download/collection-preconfezionata` | download_collection | | `tipologiche/denominazione-atto` | denominazioni | | `tipologiche/classe-provvedimento` | classi_provvedimento | | `tipologiche/estensioni` | export_formats | ## I criteri con due nomi La ricerca avanzata e l'esportazione accettano gli stessi criteri, ma tre campi cambiano nome da uno schema all'altro. La libreria traduce i nomi da sé; la tabella serve a chi confronta le risposte con la specifica. | Nome nella ricerca | Nome nell'esportazione | Parametro della libreria | | ---------------------------- | ------------------------- | ------------------------ | | `vigenza` | `dataVigenza` | `vigente_al` | | `dataInizioPubProvvedimento` | `dataInizioPubblicazione` | `pubblicazione[0]` | | `dataFinePubProvvedimento` | `dataFinePubblicazione` | `pubblicazione[1]` | Perché questa differenza sia pericolosa lo spiega [com'è fatto il servizio](https://normattiva-sdk.ireneburresi.dev/capire/il-servizio/#i-criteri-con-due-nomi). ## I campi non esposti Alcuni campi della specifica non hanno un parametro nella libreria: | Campo | Perché no | | ------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | `numeroArticolo` nell'export | non ha effetto: l'archivio torna con tutti gli articoli | | `dataVigenza` su `dettaglio-atto` | non ha effetto: la finestra restituita è la stessa con e senza | | `testoContainsType`, `titoloContainsType` | i valori ammessi non sono documentati né deducibili | | `email` sull'export | manderebbe l'archivio per posta elettronica: un effetto collaterale che una libreria non deve produrre implicitamente | | `testoInVigore`, `dataPubblicazioneInGazzetta`, `numeroFileRicerca` | sempre nulli o a zero in ogni risposta osservata | # Gli errori La regola generale e i modi di catturarli sono in [gli errori](https://normattiva-sdk.ireneburresi.dev/capire/errori/index.md). Sotto, la firma di ciascuno. ### NormattivaError Bases: `Exception` Classe base di tutti gli errori sollevati da questa libreria. ## Gli errori della richiesta Tutti anche `ValueError`. ### InvalidArgumentError Bases: `NormattivaError`, `ValueError` Un argomento passato alla libreria non è valido: non serve interrogare il servizio. ### InvalidUrnError ```python InvalidUrnError(testo: object, motivo: str | None = None) ``` Bases: `NormattivaError`, `ValueError` L'URN non rispetta la grammatica NIR che Normattiva accetta. Codice sorgente in `src/normattiva/errori.py` ```python def __init__(self, testo: object, motivo: str | None = None) -> None: self.testo = testo self.motivo = motivo messaggio = f"URN non valido: {testo!r}" if motivo: messaggio = f"{messaggio} ({motivo})" super().__init__(messaggio) ``` ### RuleViolationError ```python RuleViolationError( codice: int, messaggio: str | None = None ) ``` Bases: `NormattivaError`, `ValueError` La richiesta viola una regola documentata del servizio. Descrive sempre la richiesta, mai un problema del servizio: se il codice non è fra quelli documentati, o arriva con un `5xx`, la libreria solleva un'eccezione diversa. Codice sorgente in `src/normattiva/errori.py` ```python def __init__(self, codice: int, messaggio: str | None = None) -> None: self.codice = codice try: self.regola: RuleCode | None = RuleCode(codice) except ValueError: self.regola = None etichetta = ( self.regola.name.lower().replace("_", " ") if self.regola else "regola sconosciuta" ) super().__init__(messaggio or f"[{codice}] {etichetta}") ``` ### RuleCode Bases: `IntEnum` Codici applicativi con cui il servizio segnala un errore nella richiesta. Questi codici descrivono la richiesta, non lo stato del servizio: la stessa richiesta riceverà sempre lo stesso codice, quindi non viene mai ritentata. Un codice fuori da questo elenco non dà la stessa garanzia: può indicare anche un guasto transitorio, e in quel caso il retry segue lo stato HTTP come per ogni altra risposta. ## Gli errori restituiti dal servizio ### NotFoundError Bases: `NormattivaError` Nessun atto corrisponde alle coordinate richieste. ### AmbiguityError ```python AmbiguityError(candidati: tuple[DettaglioAtto, ...]) ``` Bases: `NormattivaError` L'URN corrisponde a più di un atto pubblicato. I candidati sono inclusi nella stessa risposta in cui è emersa l'ambiguità, quindi leggerli non costa una richiesta in più. Non portano un identificatore: si distinguono per le coordinate di Gazzetta, cioè data, numero e codice redazionale. Codice sorgente in `src/normattiva/errori.py` ```python def __init__(self, candidati: tuple[DettaglioAtto, ...]) -> None: self.candidati = candidati super().__init__( f"l'URN corrisponde a {len(candidati)} atti distinti: scegliere quale usare" ) ``` ### NotYetInForceError ```python NotYetInForceError(vigente_dal: date | None = None) ``` Bases: `NormattivaError` L'articolo non esisteva ancora alla data richiesta. Codice sorgente in `src/normattiva/errori.py` ```python def __init__(self, vigente_dal: date | None = None) -> None: self.vigente_dal = vigente_dal quando = f" (in vigore dal {vigente_dal.isoformat()})" if vigente_dal else "" super().__init__(f"l'articolo non era ancora in vigore alla data richiesta{quando}") ``` ### OverloadedError ```python OverloadedError(descrizione: str | None = None) ``` Bases: `NormattivaError` Il servizio è sovraccarico e ha rifiutato temporaneamente la richiesta. `descrizione` contiene il messaggio del servizio, se presente; è testo informativo, non un URL né un'indicazione operativa. Codice sorgente in `src/normattiva/errori.py` ```python def __init__(self, descrizione: str | None = None) -> None: self.descrizione = descrizione messaggio = "il servizio è sovraccarico" if descrizione: messaggio = f"{messaggio}: {descrizione}" super().__init__(messaggio) ``` ### RequestBlockedError Bases: `NormattivaError` Lo strato di protezione davanti all'API ha respinto la richiesta per la sua forma. ## Gli errori sul testo ### TruncationError ```python TruncationError(ultimo_comma: int) ``` Bases: `NormattivaError` Il percorso interattivo ha restituito un articolo probabilmente troncato. `ultimo_comma` è l'etichetta dell'ultimo comma ricevuto, non il numero di commi arrivati: il sospetto di troncamento nasce dall'etichetta, che cade esattamente su un multiplo di cento. Codice sorgente in `src/normattiva/errori.py` ```python def __init__(self, ultimo_comma: int) -> None: self.ultimo_comma = ultimo_comma super().__init__( f"l'articolo si ferma al comma {ultimo_comma} e potrebbe essere troncato: " "usare l'esportazione per il testo integrale" ) ``` ### ValidityMismatchError ```python ValidityMismatchError( richiesta: date, finestra: FinestraVigenza ) ``` Bases: `NormattivaError` Il servizio ha risposto con una versione che non copre la data richiesta. Codice sorgente in `src/normattiva/errori.py` ```python def __init__(self, richiesta: date, finestra: FinestraVigenza) -> None: self.richiesta = richiesta self.finestra = finestra super().__init__( f"richiesta la vigenza al {richiesta.isoformat()} ma la risposta copre {finestra}" ) ``` ### VersionNotFoundError ```python VersionNotFoundError(giorno: date) ``` Bases: `NotFoundError` Nessuna versione di questo atto copre la data richiesta. Codice sorgente in `src/normattiva/errori.py` ```python def __init__(self, giorno: date) -> None: self.giorno = giorno super().__init__(f"nessuna versione copre il {giorno.isoformat()}") ``` ### TooManyResultsError ```python TooManyResultsError(totale: int | None, massimo: int) ``` Bases: `NormattivaError` L'operazione supererebbe il limite di risultati consentito dal chiamante. Codice sorgente in `src/normattiva/errori.py` ```python def __init__(self, totale: int | None, massimo: int) -> None: self.totale = totale self.massimo = massimo quanti = f"{totale} risultati superano" if totale is not None else "i risultati superano" super().__init__( f"{quanti} il massimo di {massimo}: " "restringere la richiesta oppure alzare il limite esplicitamente" ) ``` ### ExportFailedError ```python ExportFailedError(descrizione: str | None = None) ``` Bases: `NormattivaError` Il servizio ha dichiarato fallita l'esportazione. Codice sorgente in `src/normattiva/errori.py` ```python def __init__(self, descrizione: str | None = None) -> None: self.descrizione = descrizione super().__init__(descrizione or "esportazione fallita") ``` ## Gli errori di trasporto ### ConnectionError Bases: `NormattivaError` Impossibile raggiungere il servizio, oppure la connessione si è interrotta. ### UnexpectedResponseError Bases: `NormattivaError` La risposta non è nel formato che questa libreria sa interpretare. # L'esportazione Un'esportazione è un lavoro che gira dalla parte del servizio: `Export` lo rappresenta mentre è in corso, `ExportStatus` dice a che punto è, `Progress` quanto ne resta, e `Corpus` è l'archivio una volta scaricato. `Export` sta in piedi da solo, identificato dal suo token, e sopravvive al processo che l'ha avviato. Il percorso completo, dai criteri all'archivio riaperto da disco, sta in [esportare un atto intero](https://normattiva-sdk.ireneburresi.dev/come-fare/esportare-un-atto/index.md). ## Gli stati di un'esportazione ```mermaid stateDiagram-v2 [*] --> TO_CONFIRM: start_export() TO_CONFIRM --> WAITING WAITING --> PROCESSING PROCESSING --> CONFIRMED_WITH_DELAY: ci vuole piu' tempo CONFIRMED_WITH_DELAY --> PROCESSING PROCESSING --> COMPLETED: download() PROCESSING --> FAILED PROCESSING --> OVERLOADED COMPLETED --> [*] FAILED --> [*] OVERLOADED --> [*] ``` I tre stati in fondo concludono l'attesa di `wait`; gli altri la fanno tornare a interrogare il servizio. ## Il formato dell'archivio Un ZIP con una cartella per atto, e dentro un documento JSON per versione: ```text LEGGE_19900807_241/1990-08-18_090G0294_ORIGINALE_V0.json LEGGE_19900807_241/1990-08-18_090G0294_VIGENZA_1990-12-20_V1.json LEGGE_19900807_241/1990-08-18_090G0294_VIGENZA_1991-01-23_V2.json ``` Il nome porta la data di pubblicazione in Gazzetta, il codice redazionale, la data da cui la versione vale e il suo numero progressivo. Nessun campo del documento riporta quella data: se i nomi non dichiarano la versione, `Corpus` rifiuta l'archivio con UnexpectedResponseError invece di leggerli tutti come «originale». ### Export ```python Export( token: str, trasporto: Trasporto, *, format: Format = JSON, stato: ExportStatus = TO_CONFIRM, sleep: Callable[[float], None] = sleep, clock: Callable[[], float] = monotonic, ) ``` Un'esportazione, dalla richiesta all'archivio prodotto. Codice sorgente in `src/normattiva/esporta.py` ```python def __init__( self, token: str, trasporto: Trasporto, *, format: Format = Format.JSON, stato: ExportStatus = ExportStatus.TO_CONFIRM, sleep: Callable[[float], None] = time.sleep, clock: Callable[[], float] = time.monotonic, ) -> None: self._token = token self._formato = format self._trasporto = trasporto self._stato = stato self._avanzamento = Progress() self._posizione: str | None = None self._sleep = sleep self._clock = clock ``` #### token ```python token: str ``` Il token con cui riprendere questa esportazione da un altro processo. #### format ```python format: Format ``` Il format in cui è stato richiesto l'archivio. #### status ```python status: ExportStatus ``` L'ultimo stato dichiarato dal servizio. #### progress ```python progress: Progress ``` L'ultimo avanzamento dichiarato dal servizio, quando lo dichiara. #### from_token ```python from_token( token: str, trasporto: Trasporto, *, format: Format = JSON, ) -> Export ``` Riprende un'esportazione già avviata, a partire dal suo token. Codice sorgente in `src/normattiva/esporta.py` ```python @classmethod def from_token( cls, token: str, trasporto: Trasporto, *, format: Format = Format.JSON ) -> Export: """Riprende un'esportazione già avviata, a partire dal suo token.""" esportazione = cls(token, trasporto, format=format, stato=ExportStatus.WAITING) esportazione.refresh() return esportazione ``` #### refresh ```python refresh() -> ExportStatus ``` Interroga il servizio una volta sullo stato dell'esportazione. Codice sorgente in `src/normattiva/esporta.py` ```python def refresh(self) -> ExportStatus: """Interroga il servizio una volta sullo stato dell'esportazione.""" risposta = self._trasporto.get( f"ricerca-asincrona/check-status/{self._token}", attesi=(200, 202, 303) ) stato, posizione, avanzamento = _stato_da(risposta, self._posizione) self._stato, self._posizione, self._avanzamento = stato, posizione, avanzamento return stato ``` #### wait ```python wait(*, timeout: float = TIMEOUT) -> ExportStatus ``` Interroga il servizio finché l'archivio è pronto, o finché la scadenza è superata. Se il servizio dichiara un possibile ritardo, la scadenza viene prorogata una sola volta: prorogarla a ogni dichiarazione toglierebbe ogni limite all'attesa. Parametri: | Nome | Tipo | Descrizione | Predefinito | | --------- | ------- | --------------------------------------------- | ----------- | | `timeout` | `float` | quanti secondi attendere prima di rinunciare. | `TIMEOUT` | Restituisce: | Tipo | Descrizione | | -------------- | --------------------------------------------- | | `ExportStatus` | Lo stato in cui l'esportazione si è conclusa. | Solleva: | Tipo | Descrizione | | ------------------- | ------------------------------------------------------------------------- | | `ExportFailedError` | il servizio l'ha dichiarata fallita, oppure l'attesa ha superato timeout. | | `OverloadedError` | il servizio non è in grado di completarla adesso. | Codice sorgente in `src/normattiva/esporta.py` ```python def wait(self, *, timeout: float = TIMEOUT) -> ExportStatus: """Interroga il servizio finché l'archivio è pronto, o finché la scadenza è superata. Se il servizio dichiara un possibile ritardo, la scadenza viene prorogata una sola volta: prorogarla a ogni dichiarazione toglierebbe ogni limite all'attesa. Args: timeout: quanti secondi attendere prima di rinunciare. Returns: Lo stato in cui l'esportazione si è conclusa. Raises: ExportFailedError: il servizio l'ha dichiarata fallita, oppure l'attesa ha superato `timeout`. OverloadedError: il servizio non è in grado di completarla adesso. """ limite = self._clock() + timeout prorogato = False while True: stato = self.refresh() if stato.done: return stato if stato is ExportStatus.CONFIRMED_WITH_DELAY and not prorogato: limite = self._clock() + timeout prorogato = True if self._clock() >= limite: raise ExportFailedError( f"l'esportazione non si è conclusa entro {timeout:.0f} secondi" ) logger.debug( "esportazione %s: stato %s, %s", self._token, stato.name, self._avanzamento ) self._sleep(ATTESA_FRA_CONTROLLI) ``` #### download ```python download() -> Corpus ``` Scarica l'archivio e legge gli atti che contiene. Solo il format JSON viene convertito in modelli; gli altri formati si scaricano come file con `save`, perché la libreria non li interpreta. Restituisce: | Tipo | Descrizione | | -------- | ------------------------------------------------------ | | `Corpus` | Gli atti che l'archivio contiene, e l'archivio stesso. | Solleva: | Tipo | Descrizione | | ------------------------- | ---------------------------------------------------------------------------- | | `InvalidArgumentError` | il format non è JSON; usare save. | | `UnexpectedResponseError` | l'archivio non è leggibile, o i nomi dei file non dichiarano più la vigenza. | Codice sorgente in `src/normattiva/esporta.py` ```python def download(self) -> Corpus: """Scarica l'archivio e legge gli atti che contiene. Solo il format JSON viene convertito in modelli; gli altri formati si scaricano come file con `save`, perché la libreria non li interpreta. Returns: Gli atti che l'archivio contiene, e l'archivio stesso. Raises: InvalidArgumentError: il format non è JSON; usare `save`. UnexpectedResponseError: l'archivio non è leggibile, o i nomi dei file non dichiarano più la vigenza. """ _verifica_leggibile(self._formato, "save()") return Corpus.from_data(self._scarica()) ``` #### save ```python save(path: str | Path) -> Path ``` Scarica l'archivio e lo scrive su disco, in qualunque format. Codice sorgente in `src/normattiva/esporta.py` ```python def save(self, path: str | Path) -> Path: """Scarica l'archivio e lo scrive su disco, in qualunque format.""" destinazione = Path(path) destinazione.write_bytes(self._scarica()) return destinazione ``` ### AsyncExport ```python AsyncExport( token: str, trasporto: TrasportoAsync, *, format: Format = JSON, stato: ExportStatus = TO_CONFIRM, sleep: Callable[[float], Awaitable[None]] = sleep, clock: Callable[[], float] = monotonic, ) ``` La variante asincrona di `Export`. Codice sorgente in `src/normattiva/esporta.py` ```python def __init__( self, token: str, trasporto: TrasportoAsync, *, format: Format = Format.JSON, stato: ExportStatus = ExportStatus.TO_CONFIRM, sleep: Callable[[float], Awaitable[None]] = asyncio.sleep, clock: Callable[[], float] = time.monotonic, ) -> None: self._token = token self._formato = format self._trasporto = trasporto self._stato = stato self._avanzamento = Progress() self._posizione: str | None = None self._sleep = sleep self._clock = clock ``` #### token ```python token: str ``` Il token con cui riprendere questa esportazione da un altro processo. #### format ```python format: Format ``` Il format in cui è stato richiesto l'archivio. #### status ```python status: ExportStatus ``` L'ultimo stato dichiarato dal servizio. #### progress ```python progress: Progress ``` L'ultimo avanzamento dichiarato dal servizio, quando lo dichiara. #### from_token ```python from_token( token: str, trasporto: TrasportoAsync, *, format: Format = JSON, ) -> AsyncExport ``` Riprende un'esportazione già avviata, a partire dal suo token. Codice sorgente in `src/normattiva/esporta.py` ```python @classmethod async def from_token( cls, token: str, trasporto: TrasportoAsync, *, format: Format = Format.JSON ) -> AsyncExport: """Riprende un'esportazione già avviata, a partire dal suo token.""" esportazione = cls(token, trasporto, format=format, stato=ExportStatus.WAITING) await esportazione.refresh() return esportazione ``` #### refresh ```python refresh() -> ExportStatus ``` Interroga il servizio una volta sullo stato dell'esportazione. Codice sorgente in `src/normattiva/esporta.py` ```python async def refresh(self) -> ExportStatus: """Interroga il servizio una volta sullo stato dell'esportazione.""" risposta = await self._trasporto.get( f"ricerca-asincrona/check-status/{self._token}", attesi=(200, 202, 303) ) stato, posizione, avanzamento = _stato_da(risposta, self._posizione) self._stato, self._posizione, self._avanzamento = stato, posizione, avanzamento return stato ``` #### wait ```python wait(*, timeout: float = TIMEOUT) -> ExportStatus ``` Interroga il servizio finché l'archivio è pronto, o finché la scadenza è superata. Se il servizio dichiara un possibile ritardo, la scadenza viene prorogata una sola volta: prorogarla a ogni dichiarazione toglierebbe ogni limite all'attesa. Parametri: | Nome | Tipo | Descrizione | Predefinito | | --------- | ------- | --------------------------------------------- | ----------- | | `timeout` | `float` | quanti secondi attendere prima di rinunciare. | `TIMEOUT` | Restituisce: | Tipo | Descrizione | | -------------- | --------------------------------------------- | | `ExportStatus` | Lo stato in cui l'esportazione si è conclusa. | Solleva: | Tipo | Descrizione | | ------------------- | ------------------------------------------------------------------------- | | `ExportFailedError` | il servizio l'ha dichiarata fallita, oppure l'attesa ha superato timeout. | | `OverloadedError` | il servizio non è in grado di completarla adesso. | Codice sorgente in `src/normattiva/esporta.py` ```python async def wait(self, *, timeout: float = TIMEOUT) -> ExportStatus: """Interroga il servizio finché l'archivio è pronto, o finché la scadenza è superata. Se il servizio dichiara un possibile ritardo, la scadenza viene prorogata una sola volta: prorogarla a ogni dichiarazione toglierebbe ogni limite all'attesa. Args: timeout: quanti secondi attendere prima di rinunciare. Returns: Lo stato in cui l'esportazione si è conclusa. Raises: ExportFailedError: il servizio l'ha dichiarata fallita, oppure l'attesa ha superato `timeout`. OverloadedError: il servizio non è in grado di completarla adesso. """ limite = self._clock() + timeout prorogato = False while True: stato = await self.refresh() if stato.done: return stato if stato is ExportStatus.CONFIRMED_WITH_DELAY and not prorogato: limite = self._clock() + timeout prorogato = True if self._clock() >= limite: raise ExportFailedError( f"l'esportazione non si è conclusa entro {timeout:.0f} secondi" ) logger.debug( "esportazione %s: stato %s, %s", self._token, stato.name, self._avanzamento ) await self._sleep(ATTESA_FRA_CONTROLLI) ``` #### download ```python download() -> Corpus ``` Scarica l'archivio e legge gli atti che contiene. Solo il format JSON viene convertito in modelli; gli altri formati si scaricano come file con `save`, perché la libreria non li interpreta. Restituisce: | Tipo | Descrizione | | -------- | ------------------------------------------------------ | | `Corpus` | Gli atti che l'archivio contiene, e l'archivio stesso. | Solleva: | Tipo | Descrizione | | ------------------------- | ---------------------------------------------------------------------------- | | `InvalidArgumentError` | il format non è JSON; usare save. | | `UnexpectedResponseError` | l'archivio non è leggibile, o i nomi dei file non dichiarano più la vigenza. | Codice sorgente in `src/normattiva/esporta.py` ```python async def download(self) -> Corpus: """Scarica l'archivio e legge gli atti che contiene. Solo il format JSON viene convertito in modelli; gli altri formati si scaricano come file con `save`, perché la libreria non li interpreta. Returns: Gli atti che l'archivio contiene, e l'archivio stesso. Raises: InvalidArgumentError: il format non è JSON; usare `save`. UnexpectedResponseError: l'archivio non è leggibile, o i nomi dei file non dichiarano più la vigenza. """ _verifica_leggibile(self._formato, "save()") return Corpus.from_data(await self._scarica()) ``` #### save ```python save(path: str | Path) -> Path ``` Scarica l'archivio e lo scrive su disco, in qualunque format. Codice sorgente in `src/normattiva/esporta.py` ```python async def save(self, path: str | Path) -> Path: """Scarica l'archivio e lo scrive su disco, in qualunque format.""" destinazione = Path(path) destinazione.write_bytes(await self._scarica()) return destinazione ``` ### ExportStatus Bases: `IntEnum` Stato di avanzamento di un'esportazione. #### done ```python done: bool ``` Indica se lo stato è terminale: interrogare di nuovo il servizio non lo cambierà. ### Progress ```python Progress( percent: float | None = None, processed: int | None = None, total: int | None = None, ) ``` L'avanzamento che il servizio dichiara per un'esportazione. La percentuale da sola non dice se il lavoro sta procedendo: `processed` e `total` sì, e sono l'unico modo per capire se un'esportazione lunga è ferma o solo lenta. Il servizio non li invia sempre. ### Corpus ```python Corpus(atti: tuple[AttoStorico, ...], archive: bytes = b'') ``` Gli atti contenuti in un archivio esportato, insieme all'archivio stesso. #### attribuzione ```python attribuzione: str ``` La riga di attribuzione richiesta dalla licenza. #### from_zip ```python from_zip(path: str | Path) -> Corpus ``` Riapre un'esportazione salvata in precedenza, senza accedere alla rete. Parametri: | Nome | Tipo | Descrizione | Predefinito | | ------ | ----- | ----------- | ------------------------------------------ | | `path` | \`str | Path\` | il file ZIP scritto in precedenza da save. | Restituisce: | Tipo | Descrizione | | -------- | ------------------------------------------------------ | | `Corpus` | Gli atti che l'archivio contiene, e l'archivio stesso. | Solleva: | Tipo | Descrizione | | ------------------------- | -------------------------------------------------------------------------------------------------- | | `UnexpectedResponseError` | l'archivio non è leggibile, o non segue la convenzione di nomi da cui si legge la data di vigenza. | Codice sorgente in `src/normattiva/esporta.py` ```python @classmethod def from_zip(cls, path: str | Path) -> Corpus: """Riapre un'esportazione salvata in precedenza, senza accedere alla rete. Args: path: il file ZIP scritto in precedenza da `save`. Returns: Gli atti che l'archivio contiene, e l'archivio stesso. Raises: UnexpectedResponseError: l'archivio non è leggibile, o non segue la convenzione di nomi da cui si legge la data di vigenza. """ dati = Path(path).read_bytes() return cls(atti=_wire.leggi_corpus(dati), archive=dati) ``` #### from_data ```python from_data(dati: bytes) -> Corpus ``` Legge un archivio già presente in memoria. Codice sorgente in `src/normattiva/esporta.py` ```python @classmethod def from_data(cls, dati: bytes) -> Corpus: """Legge un archivio già presente in memoria.""" return cls(atti=_wire.leggi_corpus(dati), archive=dati) ``` #### save ```python save(path: str | Path) -> Path ``` Scrive l'archivio su disco, per riaprirlo senza una nuova esportazione. Codice sorgente in `src/normattiva/esporta.py` ```python def save(self, path: str | Path) -> Path: """Scrive l'archivio su disco, per riaprirlo senza una nuova esportazione.""" destinazione = Path(path) destinazione.write_bytes(self.archive) return destinazione ``` # I modelli Tutti i modelli sono `dataclass` **congelate**: non si modificano dopo la costruzione, si hashano e si confrontano per valore. Nessuno di questi va costruito a mano nell'uso normale: arrivano dalle risposte del servizio. ## Quale oggetto arriva da quale chiamata | Chiamata | Restituisce | Contiene | | ------------------------------------ | ---------------------------- | --------------------------------------------------------- | | dettaglio | `DettaglioAtto` | il testo di un atto o articolo in una finestra di vigenza | | cronologia | iteratore di `DettaglioAtto` | una versione per volta, dalla più vecchia | | ricerca, ricerca_avanzata | `EsitoRicerca` | una pagina di `AttoTrovato`, più totale e faccette | | ricerca_completa | iteratore di `AttoTrovato` | gli atti di tutte le pagine, uno per volta | | atti_aggiornati | iteratore di `AttoTrovato` | gli atti modificati nel periodo | | start_export | `Export` | il lavoro in corso, con il suo token | | Export.download | `Corpus` | un `AttoStorico` per atto, con tutte le versioni | | denominazioni e le altre tipologiche | tupla di `Tipologica` | i codici che i criteri accettano | | collections | tupla di `Collection` | gli archivi già confezionati | ```mermaid classDiagram direction LR class DettaglioAtto { +str titolo +str testo +tuple~Comma~ commi +str note_aggiornamento +bool possibile_troncamento +str permalink } class EstremiAtto { +str denominazione +date data +str numero +str citazione } class PubblicazioneGazzetta { +date data +int numero +str codice_redazionale } class FinestraVigenza { +date inizio +date fine +bool aperta +contiene(giorno) bool } class Comma { +str numero +str testo } class EsitoRicerca { +int totale +int pagina +bool ultima_pagina } class AttoTrovato { +str titolo +bool ha_urn +Urn urn } class Faccette { +tuple per_anno +tuple per_tipo +tuple per_emettitore } DettaglioAtto *-- "1" EstremiAtto : estremi DettaglioAtto *-- "1" PubblicazioneGazzetta : gazzetta DettaglioAtto *-- "1" FinestraVigenza : finestra DettaglioAtto *-- "0..n" Comma : commi EsitoRicerca *-- "0..n" AttoTrovato : atti EsitoRicerca *-- "1" Faccette : faccette AttoTrovato *-- "1" EstremiAtto : estremi AttoTrovato *-- "1" PubblicazioneGazzetta : gazzetta ``` Dall'esportazione arriva invece un albero, dove `Partizione` contiene sé stessa: ```mermaid classDiagram direction LR class Corpus { +tuple~AttoStorico~ atti +save(path) +from_zip(path)$ Corpus } class AttoStorico { +Urn urn +bool abrogato +date pubblicato_il +alla_data(giorno) VersioneAtto +originale VersioneAtto +vigente VersioneAtto } class VersioneAtto { +date vigente_dal +bool originale +articoli() Iterator } class Partizione { +str tipo +str numero +str rubrica +str testo } class Aggiornamento { +date data +str testo } Corpus *-- "1..n" AttoStorico : atti AttoStorico *-- "1..n" VersioneAtto : versioni AttoStorico *-- "0..n" Aggiornamento : aggiornamenti VersioneAtto *-- "0..n" Partizione : articolato VersioneAtto *-- "0..n" Partizione : annessi Partizione *-- "0..n" Partizione : figli ``` I due percorsi producono modelli diversi perché le due risposte del servizio sono strutturalmente diverse: `DettaglioAtto` porta testo e commi di **una** versione, `AttoStorico` porta l'albero dell'articolato di **tutte**. Il confronto sta in [com'è fatto il servizio](https://normattiva-sdk.ireneburresi.dev/capire/il-servizio/#due-modelli-di-risposta). ## Il testo di un atto o di un articolo ### DettaglioAtto ```python DettaglioAtto( estremi: EstremiAtto, gazzetta: PubblicazioneGazzetta, titolo: str, sottotitolo: str | None, testo_html: str, finestra: FinestraVigenza | None, ) ``` Il testo di un atto o di un articolo a un punto nel tempo. Porta il testo già separato dalle note redazionali, i commi numerati, la finestra di vigenza in cui quel testo è valido, le coordinate di Gazzetta e il permalink alla pagina pubblica. Le proprietà che leggono l'HTML del servizio lo fanno alla prima richiesta e tengono il risultato. #### testo ```python testo: str ``` Il solo testo, senza le note redazionali di aggiornamento. #### commi ```python commi: tuple[Comma, ...] ``` I commi numerati, quando l'articolo è marcato come tale. #### note_aggiornamento ```python note_aggiornamento: str | None ``` Le note redazionali sulle modifiche a questo testo, se presenti. #### preambolo ```python preambolo: str | None ``` La formula introduttiva, quando la risposta la include. #### commi_presenti ```python commi_presenti: int | None ``` Quanti commi sono arrivati, o None se il testo non ne ha. #### ultimo_comma_numerato ```python ultimo_comma_numerato: int | None ``` L'etichetta dell'ultimo comma numerato, o None se non ce n'è nessuno. Non coincide con `commi_presenti`: un articolo può avere commi con etichette non numeriche, e sono le etichette a indicare dove il testo si ferma. #### possibile_troncamento ```python possibile_troncamento: bool ``` Indica se questo testo sembra troncato. Il servizio conserva gli articoli lunghi a blocchi di cento commi e ne restituisce solo il primo, senza segnalarlo. Un articolo il cui ultimo comma numerato cade esattamente su un multiplo di cento è quindi sospetto. Un articolo che finisce davvero lì resta indistinguibile da uno troncato: per questo il valore esprime un sospetto, non una certezza. #### urn ```python urn: Urn ``` L'URN dell'atto a cui questo testo appartiene. #### permalink ```python permalink: str ``` Il link pubblico di Normattiva, per verificare sulla fonte. #### attribuzione ```python attribuzione: str ``` La riga di attribuzione richiesta dalla licenza. ### Comma ```python Comma(numero: str, testo: str) ``` Un comma numerato di un articolo. ## Le coordinate di un atto ### EstremiAtto ```python EstremiAtto( denominazione: str, data: date, numero: str | None = None, codice_tipo: str | None = None, ) ``` Gli estremi che identificano un provvedimento: tipo, data e numero. #### ha_urn ```python ha_urn: bool ``` Indica se per questo tipo di atto si sa comporre l'URN. Da verificare prima di leggere `urn` scorrendo risultati di ricerca: dodici tipi di atto su trenta, quasi tutti storici, non hanno una forma NIR verificata, e per quelli `urn` solleva un'eccezione invece di indovinare. #### urn ```python urn: Urn ``` L'URN che identifica questo atto. Solleva `InvalidUrnError` per i tipi di atto la cui forma URN non è stata verificata: un URN inventato otterrebbe un 404 dal servizio, e chi lo riceve non avrebbe modo di capire che il difetto è nell'URN. `ha_urn` permette di verificarlo in anticipo, senza sollevare. Solleva: | Tipo | Descrizione | | ----------------- | ----------------------------------------------------- | | `InvalidUrnError` | la forma URN di questo tipo di atto non è verificata. | #### citazione ```python citazione: str ``` L'atto nella forma in cui si cita nella pratica giuridica italiana. ### PubblicazioneGazzetta ```python PubblicazioneGazzetta( data: date, numero: int | None = None, codice_redazionale: str | None = None, supplemento: str | None = None, numero_supplemento: int | None = None, ) ``` Dove e quando un atto è stato pubblicato in Gazzetta Ufficiale. `numero` è opzionale perché il servizio non lo fornisce ovunque: gli atti aggiornanti citati dentro un'esportazione hanno la data di Gazzetta ma non il numero, e in quel caso il campo resta `None`. #### in_supplemento ```python in_supplemento: bool ``` Se l'atto è uscito in un supplemento e non nella Gazzetta ordinaria. ### FinestraVigenza ```python FinestraVigenza(inizio: date, fine: date | None = None) ``` Intervallo di tempo in cui una versione di un testo è stata in vigore. #### aperta ```python aperta: bool ``` Se questa è la versione tuttora in vigore. #### contiene ```python contiene(giorno: date) -> bool ``` Indica se il giorno indicato cade dentro questa finestra. Una finestra aperta, cioè senza fine, contiene ogni giorno a partire dal suo inizio. Codice sorgente in `src/normattiva/modelli.py` ```python def contiene(self, giorno: date) -> bool: """Indica se il giorno indicato cade dentro questa finestra. Una finestra aperta, cioè senza fine, contiene ogni giorno a partire dal suo inizio. """ return self.inizio <= giorno and (self.fine is None or giorno <= self.fine) ``` ## I risultati di una ricerca ### EsitoRicerca ```python EsitoRicerca( atti: tuple[AttoTrovato, ...], totale: int, pagina: int = 1, pagine: int = 1, faccette: Faccette = Faccette(), ) ``` Una pagina di risultati di ricerca. Non definisce `__len__`: non sarebbe chiaro se conta i risultati di questa pagina o quelli dell'intera ricerca, e un esito con la pagina vuota ma cinquemila atti in totale risulterebbe falso dentro un `if`. Si iterano gli `atti` di questa pagina, e si legge `totale` per il conteggio complessivo. #### ultima_pagina ```python ultima_pagina: bool ``` Indica se non ci sono altre pagine da chiedere. ### AttoTrovato ```python AttoTrovato( estremi: EstremiAtto, gazzetta: PubblicazioneGazzetta, titolo: str, descrizione: str | None = None, ultima_modifica: date | None = None, atti_modificanti: tuple[str, ...] = (), evidenziazioni: tuple[Evidenziazione, ...] = (), ) ``` Un atto come restituito da una ricerca. #### atti_modificanti ```python atti_modificanti: tuple[str, ...] = () ``` I codici redazionali degli ultimi atti che hanno modificato questo, come `26G00129`. Non sono URN né titoli: sono gli identificativi di Gazzetta degli atti modificanti, e per risalire all'atto serve anche la loro data, che il servizio qui non fornisce. Osservati il 2026-08-24 nel flusso degli atti aggiornati. #### ha_urn ```python ha_urn: bool ``` Indica se per questo atto si sa comporre l'URN: da verificare prima di leggerlo. #### urn ```python urn: Urn ``` L'URN con cui chiedere il testo di questo atto. Solleva `InvalidUrnError` per i tipi di atto storici la cui forma NIR non è verificata: scorrendo i risultati conviene filtrare su `ha_urn`. #### citazione ```python citazione: str ``` L'atto nella forma in cui si cita nella pratica giuridica italiana. ### Evidenziazione ```python Evidenziazione( articolo: str | None, frammenti: tuple[str, ...] = () ) ``` Il punto in cui un termine di ricerca è stato trovato dentro un atto. ### Faccette ```python Faccette( per_anno: tuple[Faccetta, ...] = (), per_tipo: tuple[Faccetta, ...] = (), per_emettitore: tuple[Faccetta, ...] = (), ) ``` Le tre faccette che la ricerca restituisce. ### Faccetta ```python Faccetta( codice: str, conteggio: int, descrizione: str | None = None, ) ``` Un valore di una faccetta di ricerca, con quanti atti lo portano. ## L'atto intero, dall'esportazione ### AttoStorico ```python AttoStorico( urn: Urn, estremi: EstremiAtto, versioni: tuple[VersioneAtto, ...], eli: str | None = None, gazzetta: PubblicazioneGazzetta | None = None, abrogato: bool = False, aggiornamenti: tuple[Aggiornamento, ...] = (), ) ``` Un atto intero con tutte le versioni incluse nell'esportazione. #### pubblicato_il ```python pubblicato_il: date ``` La data da cui l'atto esiste: la data di Gazzetta, o in mancanza quella di emanazione. #### originale ```python originale: VersioneAtto | None ``` La versione originale dell'atto, se inclusa nell'export. #### vigente ```python vigente: VersioneAtto | None ``` La versione più recente inclusa nell'export. Per un atto mai modificato è l'originale: non esiste un testo più recente di quello di pubblicazione. #### attribuzione ```python attribuzione: str ``` La riga di attribuzione richiesta dalla licenza. #### alla_data ```python alla_data(giorno: date) -> VersioneAtto ``` Restituisce la versione dell'atto in vigore nel giorno indicato. Prima della prima modifica vale il testo originale, che nell'export non ha una data di inizio: vale la data di pubblicazione dell'atto. Un atto mai modificato ha solo quella versione, valida senza limite di tempo. Parametri: | Nome | Tipo | Descrizione | Predefinito | | -------- | ------ | ----------------------------------- | -------------- | | `giorno` | `date` | il giorno di cui si vuole il testo. | *obbligatorio* | Restituisce: | Tipo | Descrizione | | -------------- | --------------------------------------------------------- | | `VersioneAtto` | La versione in vigore quel giorno, con il suo articolato. | Solleva: | Tipo | Descrizione | | ---------------------- | -------------------------------------------------------------------------------------- | | `VersionNotFoundError` | nessuna versione copre quel giorno, tipicamente perché è anteriore alla pubblicazione. | Codice sorgente in `src/normattiva/modelli.py` ```python def alla_data(self, giorno: date) -> VersioneAtto: """Restituisce la versione dell'atto in vigore nel giorno indicato. Prima della prima modifica vale il testo originale, che nell'export non ha una data di inizio: vale la data di pubblicazione dell'atto. Un atto mai modificato ha solo quella versione, valida senza limite di tempo. Args: giorno: il giorno di cui si vuole il testo. Returns: La versione in vigore quel giorno, con il suo articolato. Raises: VersionNotFoundError: nessuna versione copre quel giorno, tipicamente perché è anteriore alla pubblicazione. """ datate = [ (v.vigente_dal, v) for v in self.versioni if v.vigente_dal is not None and v.vigente_dal <= giorno ] if datate: return max(datate, key=lambda coppia: coppia[0])[1] originale = self.originale if originale is not None and giorno >= self.pubblicato_il: return originale raise VersionNotFoundError(giorno) ``` ### VersioneAtto ```python VersioneAtto( vigente_dal: date | None, articolato: tuple[Partizione, ...] = (), annessi: tuple[Partizione, ...] = (), ) ``` Una versione dell'atto, in vigore da una certa data in poi. #### originale ```python originale: bool ``` Indica se questa è la versione originale, come pubblicata la prima volta. #### articoli ```python articoli() -> Iterator[Partizione] ``` Itera gli articoli di questa versione, in ordine. Scende solo nell'articolato: gli allegati stanno in `annessi`, che è un ramo separato dell'atto. Produce: | Tipo | Descrizione | | ------------ | -------------------------------------------- | | `Partizione` | Un articolo per volta, in ordine di lettura. | Codice sorgente in `src/normattiva/modelli.py` ```python def articoli(self) -> Iterator[Partizione]: """Itera gli articoli di questa versione, in ordine. Scende solo nell'articolato: gli allegati stanno in `annessi`, che è un ramo separato dell'atto. Yields: Un articolo per volta, in ordine di lettura. """ for nodo in self.articolato: yield from nodo.articoli() ``` ### Partizione ```python Partizione( tipo: str | None, numero: str, testo: str, rubrica: str | None = None, finestre: tuple[FinestraVigenza, ...] = (), figli: tuple[Partizione, ...] = (), ) ``` Un nodo della struttura di un atto: un capo, un articolo, un allegato. `tipo` contiene il nome NIR del nodo così come lo dichiara il servizio; per gli articoli vale la costante `ARTICOLO`. #### articoli ```python articoli() -> Iterator[Partizione] ``` Itera gli articoli a partire da questo nodo, incluso il nodo stesso. Produce: | Tipo | Descrizione | | ------------ | ------------------------------------------------------- | | `Partizione` | Ogni nodo il cui tipo è ARTICOLO, in ordine di lettura. | Codice sorgente in `src/normattiva/modelli.py` ```python def articoli(self) -> Iterator[Partizione]: """Itera gli articoli a partire da questo nodo, incluso il nodo stesso. Yields: Ogni nodo il cui `tipo` è `ARTICOLO`, in ordine di lettura. """ if self.tipo == ARTICOLO: yield self for figlio in self.figli: yield from figlio.articoli() ``` ### Aggiornamento ```python Aggiornamento( data: date, testo: str, riferimenti: tuple[RiferimentoAggiornamento, ...] = (), ) ``` Una modifica subita dall'atto, come descritta dal servizio. ### RiferimentoAggiornamento ```python RiferimentoAggiornamento( gazzetta: PubblicazioneGazzetta, articolo: str | None = None, ) ``` L'articolo che ha introdotto una modifica. ## I dizionari del servizio ### Tipologica ```python Tipologica(codice: str, descrizione: str) ``` Una voce di uno dei dizionari (tipologiche) del servizio. ### Collection ```python Collection( name: str, format: str, total_atti: int, description: str | None = None, created_at: date | None = None, ) ``` Un archivio già confezionato messo a disposizione dal servizio. ### RicercaPredefinita ```python RicercaPredefinita( nome: str, parametri: tuple[tuple[str, str], ...] = () ) ``` Una ricerca predefinita suggerita dal servizio. ## Le enumerazioni ### Format Bases: `str`, `Enum` Formati in cui il servizio può produrre un'esportazione. ### ExportMode Bases: `str`, `Enum` Quali versioni di un atto deve includere un'esportazione. ### ClasseProvvedimento Bases: `IntEnum` Stato redazionale di un atto: mai aggiornato, aggiornato o abrogato. ### Sort Bases: `str`, `Enum` L'ordinamento dei risultati di una ricerca. `NEWEST` mette per primi gli atti più recenti, `OLDEST` i più antichi. ## Le costanti ### ATTRIBUZIONE ```python ATTRIBUZIONE = "Fonte: Normattiva (https://www.normattiva.it), Istituto Poligrafico e Zecca dello Stato, in licenza CC BY 4.0. Testo non autentico e gratuito: l'unico testo ufficiale è quello pubblicato sulla Gazzetta Ufficiale a mezzo stampa." ``` La riga di attribuzione che la licenza dei dati richiede. L'avviso legale del portale richiede tre menzioni: «La riproduzione dei testi forniti nel formato elettronico è consentita purché venga menzionata la fonte, il carattere non autentico e gratuito». La riga le contiene tutte e tre, e `tests/test_licenza.py` lo verifica, perché è facile accorciare un'attribuzione senza accorgersi di aver perso una menzione. ### DENOMINAZIONI_URN ```python DENOMINAZIONI_URN = { "COSTITUZIONE": "costituzione", "DECRETO": "decreto", "DECRETO DEL CAPO PROVVISORIO DELLO STATO": "decreto.del.capo.provvisorio.dello.status", "DECRETO DEL PRESIDENTE DEL CONSIGLIO DEI MINISTRI": "decreto.del.presidente.del.consiglio.dei.ministri", "DECRETO DEL PRESIDENTE DELLA REPUBBLICA": "decreto.del.presidente.della.repubblica", "DECRETO LEGISLATIVO": "decreto.legislativo", "DECRETO LEGISLATIVO LUOGOTENENZIALE": "decreto.legislativo.luogotenenziale", "DECRETO LEGISLATIVO PRESIDENZIALE": "decreto.legislativo.presidenziale", "DECRETO LUOGOTENENZIALE": "decreto.luogotenenziale", "DECRETO MINISTERIALE": "decreto.ministeriale", "DECRETO PRESIDENZIALE": "decreto.presidenziale", "DECRETO-LEGGE": "decreto.legge", "DELIBERAZIONE": "deliberazione", "LEGGE": "legge", "LEGGE COSTITUZIONALE": "legge.costituzionale", "ORDINANZA": "ordinanza", "REGIO DECRETO": "regio.decreto", "REGIO DECRETO LEGISLATIVO": "regio.decreto.legislativo", } ``` Forma di ogni tipo di atto dentro un URN, verificata contro il servizio. Sono le diciotto denominazioni su trenta per cui un URN così composto risponde davvero. Per le altre, quasi tutte tipologie storiche come «DECRETO DEL DUCE» o «REGOLAMENTO», la forma NIR non è nota: indovinarla porta a un 404 che sembra un difetto dell'atto e invece è un URN composto male. `EstremiAtto.ha_urn` permette di verificarlo in anticipo. ### ABBREVIAZIONI ```python ABBREVIAZIONI = { "COSTITUZIONE": "Cost.", "LEGGE": "L.", "LEGGE COSTITUZIONALE": "L. cost.", "DECRETO-LEGGE": "D.L.", "DECRETO LEGISLATIVO": "D.Lgs.", "DECRETO DEL PRESIDENTE DELLA REPUBBLICA": "D.P.R.", "DECRETO DEL PRESIDENTE DEL CONSIGLIO DEI MINISTRI": "D.P.C.M.", "DECRETO MINISTERIALE": "D.M.", "REGIO DECRETO": "R.D.", "REGIO DECRETO-LEGGE": "R.D.L.", "REGIO DECRETO LEGISLATIVO": "R.D.Lgs.", } ``` Abbreviazione di ogni tipo di atto nelle citazioni. Non coincide con `DENOMINAZIONI_URN`, e la differenza è voluta: abbreviare è una convenzione editoriale applicabile anche a un atto che questa libreria non sa indirizzare, come il regio decreto-legge, mentre comporre un URN richiede di conoscere la forma esatta che il servizio accetta. Un tipo che qui non compare si cita per esteso, quindi `EstremiAtto.citazione` risponde per tutti. ### ARTICOLO ```python ARTICOLO = 'articolo' ``` Valore del campo `tipo` di `Partizione` per i nodi di tipo articolo. # Gli identificatori `Urn` rappresenta un URN NIR, l'indirizzo con cui Normattiva identifica gli atti. Si compone dai pezzi, si legge da una stringa con `parse` e si trasforma con i metodi `con_*`, che restituiscono sempre un URN nuovo. Nessuna di queste operazioni tocca la rete: un identificatore malformato viene rifiutato subito. Come si usa, con gli esempi, sta in [identificare un atto](https://normattiva-sdk.ireneburresi.dev/come-fare/identificare-un-atto/index.md). ## Le parti di un URN ```text urn:nir:stato:legge:1990-08-07;241:2~art5-com3!vig=2005-01-01 │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └── vigenza a una data │ │ │ │ │ │ │ └─────── comma │ │ │ │ │ │ └──────────── articolo │ │ │ │ │ └──────────────── allegato │ │ │ │ └────────────────── numero │ │ │ └────────────────────────── data di emanazione │ │ └─────────────────────────────────── denominazione │ └───────────────────────────────────────── autorità emanante └────────────────────────────────────────────── schema ``` | Parte | Attributo | Obbligatoria | | ------------------ | --------------- | ----------------------------------------------- | | autorità emanante | `autorita` | sì, sempre `stato` | | denominazione | `denominazione` | sì, nella forma NIR (`regio.decreto`) | | data di emanazione | `data` | no: senza, l'URN porta solo l'anno | | anno | `anno` | sì | | numero | `numero` | sì, tranne per la Costituzione | | allegato | `allegato` | solo per gli atti che rispondono da un allegato | | articolo | `articolo` | no | | comma | `comma` | no, e il servizio lo rifiuta in ingresso | | vigenza | `versione` | no | Il campo si chiama `versione` perché nella grammatica NIR il suffisso dopo l'atto individua la *versione* del documento; `vigenza` è il nome con cui la si chiede, in `con_vigenza` e in `dettaglio`. ### Urn ```python Urn( denominazione: str, anno: int, data: date | None = None, numero: str | None = None, autorita: str = "stato", allegato: str | None = None, articolo: str | None = None, comma: str | None = None, versione: date | Literal["originale"] | None = None, ) ``` Un identificatore NIR, scomposto nelle sue parti. Il suffisso di versione fa parte dell'identificatore perché i rimandi dentro il testo restituito lo includono. Vale lo stesso per il comma, che però il servizio rifiuta in ingresso: `senza_comma` restituisce l'identificatore che si può davvero usare in una richiesta. `numero`, `allegato` e `articolo` accettano anche interi e li conservano come stringhe: `numero=300` e `numero="300"` costruiscono lo stesso URN. L'articolo viene inoltre normalizzato (`"5-bis"` non è ammesso, `"5bis"` sì). #### senza_comma ```python senza_comma: Urn ``` Lo stesso URN senza il comma, che il servizio rifiuta in ingresso. #### permalink ```python permalink: str ``` Il link pubblico di Normattiva, per verificare sulla fonte. #### parse ```python parse(testo: str | Urn) -> Urn ``` Costruisce un `Urn` dalla sua forma testuale. Parametri: | Nome | Tipo | Descrizione | Predefinito | | ------- | ----- | ----------- | ----------------------------------------------------------------------- | | `testo` | \`str | Urn\` | la forma testuale, oppure un Urn già letto, che viene restituito com'è. | Restituisce: | Tipo | Descrizione | | ----- | ------------------------------------------- | | `Urn` | L'identificatore scomposto nelle sue parti. | Esempi: ```pycon >>> from normattiva import Urn >>> Urn.parse("urn:nir:stato:legge:1990-08-07;241~art5").articolo '5' ``` Solleva: | Tipo | Descrizione | | ----------------- | ------------------------------------------------------------------------- | | `InvalidUrnError` | il testo non rispetta la grammatica NIR, o porta una data che non esiste. | Codice sorgente in `src/normattiva/urn.py` ```python @classmethod def parse(cls, testo: str | Urn) -> Urn: """Costruisce un `Urn` dalla sua forma testuale. Args: testo: la forma testuale, oppure un `Urn` già letto, che viene restituito com'è. Returns: L'identificatore scomposto nelle sue parti. Examples: >>> from normattiva import Urn >>> Urn.parse("urn:nir:stato:legge:1990-08-07;241~art5").articolo '5' Raises: InvalidUrnError: il testo non rispetta la grammatica NIR, o porta una data che non esiste. """ if isinstance(testo, Urn): return testo pezzi = _GRAMMATICA.match(str(testo).strip().lower()) if pezzi is None: raise InvalidUrnError(testo) grezza = pezzi["data"] data = _leggi_data(grezza) if len(grezza) > 4 else None vigenza = pezzi["vigenza"] versione: date | Literal["originale"] | None = None if vigenza: versione = _leggi_data(vigenza) elif pezzi["originale"]: versione = "originale" return cls( denominazione=pezzi["denominazione"], anno=data.year if data else int(grezza), data=data, numero=pezzi["numero"], autorita=pezzi["autorita"], allegato=pezzi["allegato"], articolo=pezzi["articolo"], comma=pezzi["comma"], versione=versione, ) ``` #### legge ```python legge( anno: int, numero: int | str, *, articolo: int | str | None = None, data: date | None = None, ) -> Urn ``` Costruisce l'URN di una legge. Parametri: | Nome | Tipo | Descrizione | Predefinito | | ---------- | ------ | ------------------- | ----------------------------------------------------------------------------------------------------------------------- | | `anno` | `int` | anno di emanazione. | *obbligatorio* | | `numero` | \`int | str\` | numero della legge, come intero o come stringa. | | `articolo` | \`int | str | None\` | | `data` | \`date | None\` | la data esatta di emanazione. Rende l'URN più preciso e disambigua fra due atti con lo stesso numero nello stesso anno. | Esempi: ```pycon >>> from normattiva import Urn >>> str(Urn.legge(1990, 241, articolo=5)) 'urn:nir:stato:legge:1990;241~art5' ``` Codice sorgente in `src/normattiva/urn.py` ```python @classmethod def legge( cls, anno: int, numero: int | str, *, articolo: int | str | None = None, data: date | None = None, ) -> Urn: """Costruisce l'URN di una legge. Args: anno: anno di emanazione. numero: numero della legge, come intero o come stringa. articolo: l'articolo da indirizzare, se ne serve uno solo. data: la data esatta di emanazione. Rende l'URN più preciso e disambigua fra due atti con lo stesso numero nello stesso anno. Examples: >>> from normattiva import Urn >>> str(Urn.legge(1990, 241, articolo=5)) 'urn:nir:stato:legge:1990;241~art5' """ return cls._di_tipo(LEGGE, anno, numero, articolo=articolo, data=data) ``` #### decreto_legge ```python decreto_legge( anno: int, numero: int | str, *, articolo: int | str | None = None, data: date | None = None, ) -> Urn ``` Costruisce l'URN di un decreto-legge. Codice sorgente in `src/normattiva/urn.py` ```python @classmethod def decreto_legge( cls, anno: int, numero: int | str, *, articolo: int | str | None = None, data: date | None = None, ) -> Urn: """Costruisce l'URN di un decreto-legge.""" return cls._di_tipo(DECRETO_LEGGE, anno, numero, articolo=articolo, data=data) ``` #### decreto_legislativo ```python decreto_legislativo( anno: int, numero: int | str, *, articolo: int | str | None = None, data: date | None = None, ) -> Urn ``` Costruisce l'URN di un decreto legislativo. Codice sorgente in `src/normattiva/urn.py` ```python @classmethod def decreto_legislativo( cls, anno: int, numero: int | str, *, articolo: int | str | None = None, data: date | None = None, ) -> Urn: """Costruisce l'URN di un decreto legislativo.""" return cls._di_tipo(DECRETO_LEGISLATIVO, anno, numero, articolo=articolo, data=data) ``` #### dpr ```python dpr( anno: int, numero: int | str, *, articolo: int | str | None = None, data: date | None = None, ) -> Urn ``` Costruisce l'URN di un decreto del Presidente della Repubblica. Codice sorgente in `src/normattiva/urn.py` ```python @classmethod def dpr( cls, anno: int, numero: int | str, *, articolo: int | str | None = None, data: date | None = None, ) -> Urn: """Costruisce l'URN di un decreto del Presidente della Repubblica.""" return cls._di_tipo(DPR, anno, numero, articolo=articolo, data=data) ``` #### regio_decreto ```python regio_decreto( anno: int, numero: int | str, *, articolo: int | str | None = None, data: date | None = None, ) -> Urn ``` Costruisce l'URN di un regio decreto. Codice sorgente in `src/normattiva/urn.py` ```python @classmethod def regio_decreto( cls, anno: int, numero: int | str, *, articolo: int | str | None = None, data: date | None = None, ) -> Urn: """Costruisce l'URN di un regio decreto.""" return cls._di_tipo(REGIO_DECRETO, anno, numero, articolo=articolo, data=data) ``` #### con_articolo ```python con_articolo(articolo: int | str) -> Urn ``` Costruisce lo stesso atto, indirizzato a uno dei suoi articoli. Codice sorgente in `src/normattiva/urn.py` ```python def con_articolo(self, articolo: int | str) -> Urn: """Costruisce lo stesso atto, indirizzato a uno dei suoi articoli.""" return replace(self, articolo=str(articolo), comma=None) ``` #### con_vigenza ```python con_vigenza(vigenza: date | Literal['originale']) -> Urn ``` Restituisce lo stesso URN con la data di vigenza indicata. Codice sorgente in `src/normattiva/urn.py` ```python def con_vigenza(self, vigenza: date | Literal["originale"]) -> Urn: """Restituisce lo stesso URN con la data di vigenza indicata.""" return replace(self, versione=vigenza) ``` # Il progetto # Il progetto `normattiva-sdk` è un progetto indipendente, non affiliato con IPZS né con la Presidenza del Consiglio dei Ministri. - [Licenza e attribuzione](https://normattiva-sdk.ireneburresi.dev/progetto/licenza/index.md): MIT per il codice, CC BY 4.0 per i dati, e che cosa comporta l'attribuzione dovuta. - [Sviluppo](https://normattiva-sdk.ireneburresi.dev/progetto/sviluppo/index.md): come si prepara l'ambiente, si eseguono i test e si costruisce la documentazione. - [Il monitoraggio del contratto](https://normattiva-sdk.ireneburresi.dev/progetto/monitoraggio/index.md): come viene sorvegliata l'API di Normattiva, e cosa succede quando cambia. - [Diario delle modifiche](https://normattiva-sdk.ireneburresi.dev/progetto/changelog/index.md): che cosa è cambiato, versione per versione. ## La documentazione in Markdown Ogni pagina di questo sito esiste anche in Markdown, allo stesso indirizzo con `index.md` in fondo. Questa pagina, per esempio, si legge anche da . Il Markdown è ricavato dall'HTML costruito e non dal sorgente, quindi contiene anche il riferimento generato dalle docstring, che nel sorgente è una riga di direttiva, e i diagrammi restano blocchi ```` ```mermaid ````. Ci sono poi due file nel formato [llms.txt](https://llmstxt.org), pensati per chi dà la documentazione in pasto a un modello linguistico: - [`/llms.txt`](https://normattiva-sdk.ireneburresi.dev/llms.txt), l'indice di tutte le pagine con una riga di descrizione ciascuna; - [`/llms-full.txt`](https://normattiva-sdk.ireneburresi.dev/llms-full.txt), l'intera documentazione in un file solo. Il codice sta su [GitHub](https://github.com/ireneburresi/normattiva-sdk). # Diario delle modifiche Il formato segue [Keep a Changelog](https://keepachangelog.com/it/1.1.0/), e le versioni il [versionamento semantico](https://semver.org/lang/it/). ## [Unreleased](https://github.com/ireneburresi/normattiva-sdk/compare/v0.1.0...HEAD) ## [0.1.0](https://github.com/ireneburresi/normattiva-sdk/releases/tag/v0.1.0) - 2026-08-27 Prima versione. ### Added - `Normattiva` e `AsyncNormattiva`: dettaglio a una data, cronologia di un articolo, ricerca semplice e per coordinate, atti aggiornati, dizionari, collezioni preconfezionate ed esportazione asincrona. - `Urn`, con i costruttori dei tipi di atto più comuni e il permalink pubblico. - `codici`: gli atti notissimi con l'allegato attraverso cui i loro articoli rispondono. - `Corpus` e `AttoStorico`: un export si riapre da disco senza rete, e `alla_data` restituisce la versione in vigore a una data. - Il comando `normattiva`, che copre le stesse capacità dal terminale: `testo`, `cerca`, `cerca-avanzata`, `cronologia`, `aggiornati`, `esporta`, `collezioni`, `scarica-collezione`, `dizionario`, `urn`, `codici`. Con `--json` l'output diventa un documento per gli script; il codice di uscita distingue la richiesta sbagliata, l'atto non trovato e il servizio in avaria. - Un notebook Jupyter in `esempi/`, eseguito su dati reali e con gli output salvati. - Monitoraggio giornaliero del contratto dell'API su GitHub Actions. # Licenza e attribuzione Tre cose diverse, con tre regimi diversi: **questa libreria**, **i dati** che restituisce, e **il rapporto** fra il progetto e chi quei dati li pubblica. ## Questa libreria non è ufficiale `normattiva-sdk` è un **progetto indipendente della comunità**. Non è affiliato con l'Istituto Poligrafico e Zecca dello Stato, né con la Presidenza del Consiglio dei Ministri, né con Normattiva. Non è approvato, sostenuto o mantenuto da loro, e nessuno di loro risponde di quello che fa. Il nome «Normattiva» compare qui per identificare il servizio con cui la libreria dialoga, non per suggerire un rapporto che non esiste. La libreria è distribuita con licenza **MIT**. Il testo completo è nel file [`LICENSE`](https://github.com/ireneburresi/normattiva-sdk/blob/main/LICENSE) del repository. ## Da dove vengono i dati Da [dati.normattiva.it](https://dati.normattiva.it), il portale open data allestito dall'**Istituto Poligrafico e Zecca dello Stato** sotto la supervisione della Presidenza del Consiglio dei Ministri, della Camera dei Deputati e del Senato della Repubblica. Il pacchetto installato non ospita e non rielabora nulla: ogni risposta arriva dal servizio nel momento in cui viene richiesta, e la libreria si limita a tradurla in oggetti Python. Il repository e l'archivio sorgente contengono invece alcune risposte reali, registrate e ridotte, che permettono alla suite di girare senza rete: sono dati IPZS ridistribuiti in licenza CC BY 4.0, con l'attribuzione accanto ai dati in `tests/fixtures/` e in `tests/contratto/dataset/`. ## Con che licenza **Creative Commons [CC BY 4.0](https://creativecommons.org/licenses/by/4.0/deed.it)**, verificato sul portale il 24 agosto 2026. IPZS ha aperto i dati per fasi, e la fase con la clausola non commerciale è terminata: | Da quando | Licenza | Che cosa copre | | ----------------------------------------- | ---------------- | ----------------------------------------------------------- | | fase sperimentale, fino al 30 giugno 2025 | CC BY 4.0 **NC** | funzionalità ridotte | | 1° luglio 2025 | CC BY 4.0 | gli stessi dati, senza la clausola NC | | **1° gennaio 2026** | **CC BY 4.0** | **tutti gli atti, in originale, a una data e multivigente** | Dal 1° gennaio 2026 vale quindi la CC BY 4.0 semplice: **l'uso commerciale e la ridistribuzione sono consentiti**, e l'unico obbligo è l'attribuzione. Una copia scaricata durante la fase sperimentale resta però soggetta alla licenza sotto cui è stata ottenuta, clausola non commerciale compresa. ## L'attribuzione è dovuta, e richiede tre menzioni L'avviso legale del portale non chiede una generica riga di cortesia. Chiede che chi riproduce i testi menzioni **la fonte**, il **carattere non autentico** e il **carattere gratuito**. La libreria espone l'attribuzione già completa di tutte e tre le menzioni: ```python atto.attribuzione corpus.attribuzione ``` ```text Fonte: Normattiva (https://www.normattiva.it), Istituto Poligrafico e Zecca dello Stato, in licenza CC BY 4.0. Testo non autentico e gratuito: l'unico testo ufficiale è quello pubblicato sulla Gazzetta Ufficiale a mezzo stampa. ``` L'attribuzione passa a chi ripubblica L'obbligo passa a te nel momento in cui ridistribuisci. Non basta che la libreria conosca l'attribuzione: deve arrivare a chi legge il tuo prodotto. Non è possibile accorciarla e restare conformi: le tre menzioni che l'avviso legale richiede devono esserci tutte e tre. ## Il testo non è ufficiale Il testo di Normattiva è una **ricostruzione redazionale**: le modifiche successive sono state applicate al testo originale da una redazione, che può sbagliare. La raccolta, per quanto vasta, è frutto di una selezione redazionale. **L'unico testo ufficiale e definitivo è quello pubblicato sulla Gazzetta Ufficiale a mezzo stampa, che prevale in caso di discordanza.** I dati sono forniti a scopo informativo. La Presidenza del Consiglio dei Ministri e IPZS non rispondono di eventuali errori o imprecisioni, né dei danni conseguenti a decisioni prese consultando il portale. A maggior ragione non ne risponde questa libreria, che è un progetto indipendente e senza garanzie. Per questo ogni `DettaglioAtto` porta il `permalink` alla pagina pubblica e le coordinate di Gazzetta: conviene che un documento costruito su questi dati li includa entrambi, così chi lo legge può risalire alla fonte e verificare. ```python atto.permalink # https://www.normattiva.it/uri-res/N2Ls?urn:nir:... atto.gazzetta # G.U. n. 192 del 1990-08-18 ``` ## Verso il servizio Il servizio è gratuito, non pubblica quote e non garantisce un livello di servizio. La libreria si autolimita a due richieste al secondo e si presenta con uno User-Agent che la identifica. Sono scelte di cortesia più che obblighi tecnici, e mantenerle resta a carico di chi usa la libreria: ```python Normattiva(user_agent="il-mio-servizio/1.2 (+https://esempio.it/contatti)") ``` Vedi [l'affidabilità](https://normattiva-sdk.ireneburresi.dev/capire/affidabilita/index.md). ## Dove leggere le fonti - [dati.normattiva.it](https://dati.normattiva.it): il portale, con avviso legale, informativa e licenza d'uso - [Come scaricare i dati](https://dati.normattiva.it/come-fare-per): i formati, le collezioni e le API - [Normattiva](https://www.normattiva.it): il portale di consultazione # Il monitoraggio del contratto Il rischio più serio per una libreria che parla con un servizio di terzi non è un difetto proprio: è che il servizio cambi senza che nessuno se ne accorga, finché il problema non arriva a chi la usa. L'API di Normattiva non ha una specifica pubblicata a cui il servizio si impegni, quindi un cambiamento può comparire in qualunque momento. Ogni notte, alle 05:17 UTC, una suite interroga la produzione su tutti e quindici gli endpoint e confronta le risposte con un riferimento registrato. Per il solo riassunto basta [l'affidabilità](https://normattiva-sdk.ireneburresi.dev/capire/affidabilita/#il-monitoraggio). ## Le impronte Ogni risposta viene ridotta a un'*impronta*: l'insieme dei cammini che contiene, con i tipi osservati lungo ciascuno. I valori non entrano nel confronto, perché cambiano di continuo ed è normale che lo facciano. Conta che i campi ci siano, e che siano del tipo registrato. | Scostamento | Esito | Perché | | ---------------------------- | ----------------- | ----------------------------------------------------- | | un campo sparisce | **fallisce** | il codice che lo leggeva si rompe | | un campo cambia tipo | **fallisce** | idem | | un campo diventa anche nullo | passa | il codice che lo trattava come opzionale regge | | compare un campo nuovo | passa, con avviso | è un'opportunità, non un guasto | | l'endpoint non risponde | **salta** | il servizio è in avaria; il contratto è un'altra cosa | Un servizio in avaria fa fallire tutti i test insieme, e un monitoraggio che segnala ogni disservizio come scostamento smette di essere letto. Un unico gestore trasforma quindi ogni `ConnectionError` in uno skip motivato; uno scostamento vero continua a fallire. ## Cosa verifica oltre le impronte **I percorsi.** Le sequenze d'uso reali: cercare e poi leggere, esportare e poi riaprire da disco, riagganciarsi a un export dal token, percorrere tutta la storia di un articolo. **I comportamenti.** Che l'articolo lungo sia ancora troncato, che l'URN ambiguo restituisca ancora due candidati, che gli articoli dei codici rispondano solo dal loro allegato, che i nomi dei file dell'export dichiarino ancora la vigenza. Sono i comportamenti su cui la libreria fa affidamento, e il test serve ad accorgersi del giorno in cui smettono di essere veri. **I valori cablati.** Le enum, le abbreviazioni delle citazioni, la mappa degli allegati: decisioni prese osservando il servizio una volta sola, che qui vengono ricontrollate. ## Chi controlla che la copertura resti Un test legge il sorgente della suite di contratto e fallisce se un metodo pubblico, una proprietà o un errore smette di comparirvi. Le poche esclusioni riportano la ragione per cui sono escluse. Anche il client asincrono viene esercitato contro il servizio reale, non solo su risposte simulate. ## Eseguirlo ```bash uv run pytest -m rete # tutto il monitoraggio uv run pytest -m rete -k "not slow" # senza il giro completo dell'export ``` La suite predefinita non tocca la rete: `-m "not rete"` è nella configurazione, così nessuno interroga la produzione per sbaglio. ## Quando qualcosa cambia Il workflow apre una issue etichettata `contratto` con il report, e la chiude quando lo scostamento rientra. Se lo scostamento è la nuova normalità, si accetta rigenerando il riferimento: ```bash uv run python -m tests.contratto.registra ``` Va fatto a mano e con criterio: rigenerare significa dichiarare che il nuovo comportamento è quello corretto. # Sviluppo Come si prepara l'ambiente, si eseguono le prove e si costruisce la documentazione di questo repository. Per installare il pacchetto in un progetto, vedi [installare la libreria](https://normattiva-sdk.ireneburresi.dev/come-fare/installare/index.md). ## Preparare l'ambiente Serve [uv](https://docs.astral.sh/uv/): ```bash git clone https://github.com/ireneburresi/normattiva-sdk cd normattiva-sdk uv sync --all-groups ``` ## I test ```bash uv run pytest # la suite offline, su risposte reali registrate uv run pytest -m rete # i test di contratto contro il servizio reale ``` La suite predefinita non tocca la rete: `-m "not rete"` è nella configurazione, così nessuno interroga la produzione per sbaglio. I test di contratto costituiscono il [monitoraggio del contratto](https://normattiva-sdk.ireneburresi.dev/progetto/monitoraggio/index.md), che gira ogni notte su GitHub Actions e apre una issue se l'API cambia. ## Lint, formato e tipi ```bash uv run ruff check uv run ruff format uv run ty check src ``` Le stesse verifiche girano in pre-commit e in CI, su Python da 3.10 a 3.14. ## La documentazione ```bash uv run mkdocs serve uv run mkdocs build --strict ``` `--strict` fallisce su qualunque link rotto o riferimento non risolto, ed è la modalità con cui la CI costruisce il sito. ### I diagrammi Mermaid gira nel browser: `mkdocs build` non ne verifica la sintassi, e un diagramma sbagliato compare come blocco di testo grezzo. La suite controlla solo gli errori più comuni (tipo dichiarato, etichette chiuse, archi tratteggiati scritti bene). Per la verifica vera, con il sito servito in locale, si apre la console del browser e si esegue: ```javascript const mermaid = (await import("https://unpkg.com/mermaid@11/dist/mermaid.esm.min.mjs")).default; const sitemap = await (await fetch("/normattiva-sdk/sitemap.xml")).text(); for (const [, url] of sitemap.matchAll(/([^<]+)<\/loc>/g)) { const html = await (await fetch(new URL(url).pathname)).text(); const pagina = new DOMParser().parseFromString(html, "text/html"); for (const blocco of pagina.querySelectorAll("pre.mermaid")) { await mermaid.parse(blocco.textContent).catch((e) => console.error(url, e.message)); } } ``` Nessun errore in console vuol dire che tutti i diagrammi del sito si disegnano. Il [riferimento](https://normattiva-sdk.ireneburresi.dev/riferimento/index.md) è generato dalle docstring con mkdocstrings, quindi le firme si aggiornano dal codice. Un test compila ogni blocco Python di queste pagine, esegue quelli autosufficienti e verifica che ogni riferimento incrociato punti a qualcosa che esiste.