Skip to content

Commit 8853520

Browse files
committed
Release v2.1.8: OpenData AKN fallback
1 parent 02d7bee commit 8853520

11 files changed

Lines changed: 627 additions & 51 deletions

File tree

DEVELOPMENT.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,9 @@ source .venv/bin/activate # Linux/macOS
7474
# or
7575
.venv\Scripts\activate # Windows
7676

77+
# Install the package in editable mode so tests can import `normattiva2md`
78+
pip3 install -e .
79+
7780
# Then run tests
7881
make test
7982
```
@@ -91,17 +94,25 @@ make test
9194

9295
```bash
9396
source .venv/bin/activate
97+
pip3 install -e .
9498
python3 -m unittest discover -s tests
9599
```
96100

97101
### Alternative: pytest (requires install)
98102

99103
```bash
100104
source .venv/bin/activate
105+
pip3 install -e .
101106
pip3 install pytest
102107
python3 -m pytest tests/ -v
103108
```
104109

110+
### Note on network-dependent tests
111+
112+
- Some tests hit external services (Normattiva, Exa API, programmagoverno.gov.it).
113+
- These can fail due to network or API availability; in that case, the failures are not necessarily regressions.
114+
- For consistent results, ensure network access and configure `EXA_API_KEY` if running Exa tests.
115+
105116
### Coverage Testing
106117

107118
**Current coverage: 40%** (Target: 70%+)

LOG.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1467,5 +1467,13 @@ Creata pianificazione completa per rendere normattiva2md usabile da notebook e s
14671467
## 2026-01-01
14681468
- Improved CLI help output with Rich library for better readability and organization
14691469

1470+
## 2026-01-29
1471+
### Fallback OpenData AKN + flag for OpenData
1472+
1473+
- **Fallback automatico**: se l'export Akoma (`caricaAKN`) non è disponibile, usa OpenData AKN (ZIP) per recuperare il file XML.
1474+
- **Nuovo flag CLI**: `--opendata` per forzare il download via OpenData su qualsiasi norma.
1475+
- **API Python**: `convert_url(..., force_opendata=True)` per forzare OpenData.
1476+
- **Selezione XML**: scelta automatica del file di vigenza più adatto dal pacchetto ZIP.
1477+
14701478
## 2026-01-21
14711479
- Bumped version to v2.1.7 for upcoming release

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,9 @@ La CLI riconosce automaticamente gli URL di `normattiva.it` e scarica il documen
233233
# Conversione diretta URL → Markdown (output su file)
234234
normattiva2md "https://www.normattiva.it/uri-res/N2Ls?urn:nir:stato:legge:2022;53" legge.md
235235

236+
# Forza download via OpenData (quando manca export Akoma)
237+
normattiva2md --opendata "https://www.normattiva.it/uri-res/N2Ls?urn:nir:stato:legge:2022;53" legge.md
238+
236239
# Conversione diretta con output su stdout (utile per pipe)
237240
normattiva2md "https://www.normattiva.it/uri-res/N2Ls?urn:nir:stato:decreto.legislativo:2005-03-07;82"
238241

@@ -438,7 +441,8 @@ opzioni:
438441
File Markdown di output (default: stdout)
439442
-s SEARCH, --search SEARCH
440443
Cerca documento in linguaggio naturale (richiede Exa AI API)
441-
--keep-xml Mantiene il file XML temporaneo dopo la conversione
444+
--keep-xml Mantiene il file XML temporaneo dopo la conversione
445+
--opendata Forza download Akoma Ntoso via API OpenData (ZIP AKN)
442446
-q, --quiet Modalità silenziosa (nessun output su stderr)
443447
-c, --completo Forza download completo anche con URL articolo-specifico
444448
--with-references Scarica anche tutti i riferimenti legislativi citati

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
44

55
[project]
66
name = "normattiva2md"
7-
version = "2.1.7"
7+
version = "2.1.8"
88
description = "Convertitore da XML Akoma Ntoso a formato Markdown con download automatico delle leggi citate e cross-references inline (CLI: normattiva2md)"
99
readme = "README.md"
1010
requires-python = ">=3.7"

setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ def read_readme():
1919

2020
setup(
2121
name="normattiva2md",
22-
version="2.1.7",
22+
version="2.1.8",
2323
description="Convertitore da XML Akoma Ntoso a formato Markdown con download automatico delle leggi citate e cross-references inline (CLI: normattiva2md)",
2424
long_description=read_readme(),
2525
long_description_content_type="text/markdown",

src/normattiva2md/api.py

Lines changed: 42 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@
3535
from .models import ConversionResult, SearchResult
3636
from .normattiva_api import (
3737
download_akoma_ntoso,
38+
download_akoma_ntoso_via_export,
39+
download_akoma_ntoso_via_opendata,
3840
extract_params_from_normattiva_url,
3941
is_normattiva_url,
4042
normalize_normattiva_url,
@@ -54,6 +56,7 @@ def convert_url(
5456
url: str,
5557
article: Optional[str] = None,
5658
with_urls: bool = False,
59+
force_opendata: bool = False,
5760
quiet: bool = False,
5861
) -> Optional[ConversionResult]:
5962
"""
@@ -82,6 +85,9 @@ def convert_url(
8285
8386
>>> # Con link inline
8487
>>> result = convert_url("https://...", with_urls=True)
88+
89+
>>> # Forza download via OpenData
90+
>>> result = convert_url("https://...", force_opendata=True)
8591
"""
8692
# Load .env file for API keys
8793
load_env_file()
@@ -100,30 +106,48 @@ def convert_url(
100106
logger.info(f"Conversione URL: {normalized_url}")
101107

102108
# Extract parameters from URL
103-
params, session = extract_params_from_normattiva_url(normalized_url, quiet=quiet)
104-
105-
if not params:
106-
logger.warning(f"Impossibile estrarre parametri da {normalized_url}")
107-
return None
109+
params = None
110+
session = None
111+
if not force_opendata:
112+
params, session = extract_params_from_normattiva_url(
113+
normalized_url, quiet=quiet
114+
)
108115

109116
# Download XML to temp file
110117
with tempfile.NamedTemporaryFile(suffix=".xml", delete=False) as tmp:
111118
xml_path = tmp.name
112119

113120
try:
114-
success = download_akoma_ntoso(params, xml_path, session, quiet=quiet)
115-
if not success:
116-
logger.warning(f"Download fallito per {url}")
117-
return None
118-
119-
# Build metadata
120-
metadata = {
121-
"dataGU": params["dataGU"],
122-
"codiceRedaz": params["codiceRedaz"],
123-
"dataVigenza": params["dataVigenza"],
124-
"url": normalized_url,
125-
"url_xml": f"https://www.normattiva.it/do/atto/caricaAKN?dataGU={params['dataGU']}&codiceRedaz={params['codiceRedaz']}&dataVigenza={params['dataVigenza']}",
126-
}
121+
if params and not force_opendata:
122+
success = download_akoma_ntoso(params, xml_path, session, quiet=quiet)
123+
if not success:
124+
logger.warning(f"Download fallito per {url}")
125+
return None
126+
127+
# Build metadata
128+
metadata = {
129+
"dataGU": params["dataGU"],
130+
"codiceRedaz": params["codiceRedaz"],
131+
"dataVigenza": params["dataVigenza"],
132+
"url": normalized_url,
133+
"url_xml": (
134+
"https://www.normattiva.it/do/atto/caricaAKN"
135+
f"?dataGU={params['dataGU']}"
136+
f"&codiceRedaz={params['codiceRedaz']}"
137+
f"&dataVigenza={params['dataVigenza']}"
138+
),
139+
}
140+
else:
141+
success, metadata, session = download_akoma_ntoso_via_opendata(
142+
normalized_url, xml_path, session=session, quiet=quiet
143+
)
144+
if not success:
145+
success, metadata, session = download_akoma_ntoso_via_export(
146+
normalized_url, xml_path, session=session, quiet=quiet
147+
)
148+
if not success:
149+
logger.warning(f"Download fallito per {url}")
150+
return None
127151

128152
# Add article to metadata if specified
129153
if article:

src/normattiva2md/cli.py

Lines changed: 69 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,9 @@ def print_rich_help():
138138
proc_table.add_column("Description")
139139

140140
proc_table.add_row("--with-urls", "Genera link agli URL normattiva.it")
141+
proc_table.add_row(
142+
"--opendata", "Forza download Akoma Ntoso via API OpenData (ZIP AKN)"
143+
)
141144
proc_table.add_row("--with-references", "Scarica anche leggi citate")
142145
proc_table.add_row("--provvedimenti", "Esporta provvedimenti attuativi CSV")
143146
proc_table.add_row("--validate", "Validazione strutturale del Markdown")
@@ -207,6 +210,8 @@ def print_rich_help():
207210
validate_normattiva_url,
208211
extract_params_from_normattiva_url,
209212
download_akoma_ntoso,
213+
download_akoma_ntoso_via_export,
214+
download_akoma_ntoso_via_opendata,
210215
)
211216
from .exa_api import lookup_normattiva_url
212217
from .akoma_utils import parse_article_reference
@@ -405,6 +410,11 @@ def main():
405410
action="store_true",
406411
help="Genera link Markdown agli URL originali di normattiva.it per gli articoli citati",
407412
)
413+
parser.add_argument(
414+
"--opendata",
415+
action="store_true",
416+
help="Forza download Akoma Ntoso via API OpenData (ZIP AKN)",
417+
)
408418
parser.add_argument(
409419
"-q", "--quiet", action="store_true", help="Disabilita output non essenziali"
410420
)
@@ -653,46 +663,79 @@ def main():
653663
f"Rilevato riferimento articolo: {article_ref}", file=sys.stderr
654664
)
655665

656-
# Estrai parametri dalla pagina
657-
params, session = extract_params_from_normattiva_url(
658-
input_source, quiet=quiet_mode
659-
)
660-
if not params:
661-
print("❌ Impossibile estrarre parametri dall'URL", file=sys.stderr)
662-
sys.exit(1)
666+
# Estrai parametri dalla pagina (se non forziamo OpenData)
667+
params = None
668+
session = None
669+
if not args.opendata:
670+
params, session = extract_params_from_normattiva_url(
671+
input_source, quiet=quiet_mode
672+
)
663673

664674
if not quiet_mode:
665-
print(f"\nParametri estratti:", file=sys.stderr)
666-
print(f" dataGU: {params['dataGU']}", file=sys.stderr)
667-
print(f" codiceRedaz: {params['codiceRedaz']}", file=sys.stderr)
668-
print(f" dataVigenza: {params['dataVigenza']}\n", file=sys.stderr)
675+
if params:
676+
print(f"\nParametri estratti:", file=sys.stderr)
677+
print(f" dataGU: {params['dataGU']}", file=sys.stderr)
678+
print(f" codiceRedaz: {params['codiceRedaz']}", file=sys.stderr)
679+
print(f" dataVigenza: {params['dataVigenza']}\n", file=sys.stderr)
680+
elif args.opendata:
681+
print(
682+
"⚠️ OpenData forzato: download via OpenData (ZIP AKN)",
683+
file=sys.stderr,
684+
)
685+
else:
686+
print(
687+
"⚠️ Link caricaAKN non disponibile: tentativo fallback OpenData",
688+
file=sys.stderr,
689+
)
669690

670691
# Crea file XML temporaneo con tempfile module (più sicuro)
671692
temp_fd, xml_temp_path = tempfile.mkstemp(
672-
suffix=f"_{params['codiceRedaz']}.xml", prefix="akoma2md_"
693+
suffix=f"_{params['codiceRedaz'] if params else 'export'}.xml",
694+
prefix="akoma2md_",
673695
)
674696
os.close(temp_fd) # Close file descriptor, we'll write with requests
675697

676698
# Scarica XML
677-
if not download_akoma_ntoso(
678-
params, xml_temp_path, session, quiet=quiet_mode
679-
):
680-
print("❌ Errore durante il download del file XML", file=sys.stderr)
681-
sys.exit(1)
699+
if params and not args.opendata:
700+
if not download_akoma_ntoso(
701+
params, xml_temp_path, session, quiet=quiet_mode
702+
):
703+
print(
704+
"❌ Errore durante il download del file XML",
705+
file=sys.stderr,
706+
)
707+
sys.exit(1)
708+
metadata = {
709+
"dataGU": params["dataGU"],
710+
"codiceRedaz": params["codiceRedaz"],
711+
"dataVigenza": params["dataVigenza"],
712+
"url": input_source,
713+
"url_xml": (
714+
"https://www.normattiva.it/do/atto/caricaAKN"
715+
f"?dataGU={params['dataGU']}"
716+
f"&codiceRedaz={params['codiceRedaz']}"
717+
f"&dataVigenza={params['dataVigenza']}"
718+
),
719+
}
720+
else:
721+
success, metadata, session = download_akoma_ntoso_via_opendata(
722+
input_source, xml_temp_path, session=session, quiet=quiet_mode
723+
)
724+
if not success:
725+
success, metadata, session = download_akoma_ntoso_via_export(
726+
input_source, xml_temp_path, session=session, quiet=quiet_mode
727+
)
728+
if not success:
729+
print(
730+
"❌ Errore durante il download via fallback OpenData/HTML",
731+
file=sys.stderr,
732+
)
733+
sys.exit(1)
682734

683735
# Converti a Markdown
684736
if not quiet_mode:
685737
print(f"\nConversione in Markdown...", file=sys.stderr)
686738

687-
# Prepare metadata dict for front matter
688-
metadata = {
689-
"dataGU": params["dataGU"],
690-
"codiceRedaz": params["codiceRedaz"],
691-
"dataVigenza": params["dataVigenza"],
692-
"url": input_source, # The original URL
693-
"url_xml": f"https://www.normattiva.it/do/atto/caricaAKN?dataGU={params['dataGU']}&codiceRedaz={params['codiceRedaz']}&dataVigenza={params['dataVigenza']}",
694-
}
695-
696739
# Add article reference to metadata if present (or if overridden by --completo)
697740
if article_ref:
698741
metadata["article"] = article_ref

src/normattiva2md/constants.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,4 @@
77
MAX_FILE_SIZE_MB = 50
88
MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024
99
DEFAULT_TIMEOUT = 30
10-
VERSION = "2.1.7"
10+
VERSION = "2.1.8"

0 commit comments

Comments
 (0)