Corso Python / Progetto API: raccogliere JSON e aggiornare SQLite
Per raccogliere dati da un’API senza duplicarli a ogni esecuzione, verifica l’intera risposta e salva i record usando un identificatore esterno stabile. Questo progetto Python legge JSON via HTTP e usa una transazione SQLite per inserire nuove spese o aggiornare nome e importo di un ID già presente.
Definire identità, aggiornamenti e limiti
L’operazione upsert inserisce un record se la chiave non esiste e lo aggiorna in caso di conflitto sulla chiave. Ha senso solo se il servizio garantisce che lo stesso id identifichi la stessa spesa. Nome e importo uguali non bastano a stabilire che due transazioni siano la stessa.
In questa versione, se una risposta contiene due record con lo stesso ID, prevale l’ultimo. Una risposta successiva può aggiornare il record, ma non elimina automaticamente gli ID assenti: non è una sincronizzazione completa dello stato remoto. Il programma controlla prima tutte le righe e solo dopo apre il database, così un dato non valido evita l’inizio delle scritture.
Usa Python 3.12 o successivo, necessario per il parametro autocommit di sqlite3.connect. Servono i moduli standard json, urllib.request e sqlite3; non servono pacchetti pip. La documentazione di sqlite3 descrive controllo delle transazioni e parametri SQL. La transazione locale non comprende la richiesta HTTP: non rende rete e database una transazione distribuita.
Preparare la risposta e due terminali
Crea una cartella nuova, distinta dagli altri esercizi, e salva questo contenuto UTF-8 come expenses.json:
[{"id":"e1","name":"coffee","amount":3500},{"id":"e2","name":"lunch","amount":8000}]
Apri un primo terminale in quella cartella, seleziona l’ambiente Python e avvia il server locale:
python -m http.server 8000 --bind 127.0.0.1
Il server è vincolato a 127.0.0.1 e serve i file della cartella corrente. Tieni il terminale aperto. Se la porta 8000 è occupata, scegli una porta libera e sostituiscila sia nel comando del server sia nell’URL passato al raccoglitore, senza arrestare processi altrui.
Apri un secondo terminale nella stessa cartella, con lo stesso ambiente. Il file expenses.json sarà letto dal server, mentre collected.db verrà creato nella cartella corrente del client. In altre cartelle, gli stessi nomi relativi possono riferirsi a file diversi.
Eseguire il raccoglitore completo
Salva il seguente programma completo come collect.py:
import json
import sqlite3
import sys
from urllib.request import urlopen
with urlopen(sys.argv[1], timeout=5) as response:
if response.status != 200:
raise ValueError("unexpected status")
raw = response.read(1_000_001)
if len(raw) > 1_000_000:
raise ValueError("response too large")
data = json.loads(raw.decode("utf-8"))
if not isinstance(data, list) or len(data) > 10000:
raise ValueError("invalid batch")
MAX_AMOUNT = 1_000_000_000_000
records = []
for row in data:
if not isinstance(row, dict):
raise ValueError("invalid row")
key, name, amount = row.get("id"), row.get("name"), row.get("amount")
if not isinstance(key, str) or not key.strip() or not isinstance(name, str) or not name.strip():
raise ValueError("invalid identity")
if type(amount) is not int or not 0 <= amount <= MAX_AMOUNT:
raise ValueError("invalid amount")
records.append((key, name, amount))
con = sqlite3.connect("collected.db", autocommit=False)
try:
with con:
con.execute("CREATE TABLE IF NOT EXISTS expenses (id TEXT PRIMARY KEY, name TEXT NOT NULL, amount INTEGER NOT NULL CHECK(amount BETWEEN 0 AND 1000000000000))")
con.executemany("INSERT INTO expenses VALUES (?, ?, ?) ON CONFLICT(id) DO UPDATE SET name=excluded.name, amount=excluded.amount", records)
count = con.execute("SELECT COUNT(*) FROM expenses").fetchone()[0]
total = sum(row[0] for row in con.execute("SELECT amount FROM expenses"))
print("stored:", count, "total:", total)
finally:
con.close()
Nel secondo terminale avvia il programma con l’URL effettivo della risposta:
python collect.py http://127.0.0.1:8000/expenses.json
La prima esecuzione deve stampare stored: 2 total: 11500 e terminare con codice 0. Ripetendo la richiesta invariata devi ottenere ancora due righe, non quattro. Modificando solo l’importo di e2 da 8000 a 9000 nel JSON e rilanciando il client devi ottenere stored: 2 total: 12500.
La risposta HTTP deve avere stato 200, essere UTF-8 con JSON valido e non superare 1.000.000 di byte. Il programma legge al massimo un byte oltre quel limite per rilevare un corpo troppo grande. La lista può contenere al massimo 10.000 elementi; ogni elemento deve avere ID e nome testuali non vuoti e un importo intero tra 0 e 1.000.000.000.000 inclusi. Il controllo sul tipo esclude anche i booleani. Una stringa come "8000" non viene convertita automaticamente.
Gli spazi vengono usati soltanto per verificare che ID e nome non siano vuoti; i valori salvati conservano il testo originale. Perciò e1 e e1 sono chiavi diverse. La lista vuota è ammessa e non cancella le spese già presenti. Definisci una normalizzazione o una politica di cancellazione separata se il servizio la richiede.
executemany passa i valori attraverso segnaposto ?. Con autocommit=False il blocco with con conferma la transazione riuscita o annulla le modifiche della transazione quando un’eccezione ne provoca l’uscita; il finally chiude la connessione. Una tabella già esistente non viene aggiornato da CREATE TABLE IF NOT EXISTS: per dati reali serve una migrazione esplicita.
Ogni importo è limitato prima di scrivere. Per il totale, il programma legge gli interi e usa sum di Python: un archivio che cresce nel tempo potrebbe superare il limite dell’aggregato intero SQL anche se ogni valore singolo è valido. Non sostituire il totale monetario con total() in virgola mobile. La documentazione SQLite di sum e total chiarisce questa differenza.
Controllare aggiornamenti, errori e completamento
- Esegui due volte il client con la risposta originale e confronta conteggio e totale: due righe e 11.500 entrambe le volte.
- Cambia solo
e2a 9000: il conteggio resta due e il totale diventa 12.500. - Trasforma l’importo in una stringa JSON. L’esecuzione deve fallire prima di scrivere e il database precedente deve mantenere i dati validi. Controlla traceback e codice di uscita immediatamente dopo il comando.
- Ripristina il JSON valido, verifica nuovamente il risultato e termina il server dal suo primo terminale con Ctrl+C.
Il timeout=5 limita le operazioni bloccanti della richiesta; non è una promessa che l’intero flusso finisca sempre entro cinque secondi. Stato HTTP, connessione, decodifica e validazione possono produrre eccezioni diverse. In questo esercizio non vengono nascoste: un processo chiamante deve controllare il fallimento e fermare le operazioni dipendenti. La documentazione di urlopen descrive risposta ed errori.
Un servizio esterno richiede anche autenticazione, paginazione, limiti di richiesta e regole per modifiche e cancellazioni. Alcune pagine riuscite non dimostrano che la raccolta sia completa. Non trasformare ogni errore in una lista vuota e non aggiungere tentativi infiniti: perderesti la distinzione tra assenza di dati e raccolta fallita. La lezione sul client HTTP e sugli errori JSON e quella sulle transazioni con sqlite3 approfondiscono i due confini del programma.




