# 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.