In questa lezione impari a
- progettare gli endpoint di un'API REST
- restituire JSON con i codici di stato corretti
- ricevere JSON in una richiesta POST e validarlo
- provare un'API con Thunder Client e con Python
- usare l'API da una pagina web con JavaScript
Le pagine della bacheca sono fatte per le persone. Ma se domani qualcuno volesse un'app per il telefono, un bot per Telegram o uno schermo nell'atrio della scuola che mostra gli avvisi? Non dovrebbero "leggere" l'HTML: serve un'API, cioè degli indirizzi che restituiscono solo dati, in formato JSON.
Il kit della lezione
Avviatelo e aprite /api/annunci nel browser: vedete i dati "nudi". Poi aprite la pagina Live dal menu, e in un'altra scheda pubblicate un annuncio: entro 5 secondi compare nella pagina Live, senza ricaricare.
Un'applicazione, due "porte"
βββββββββββββββββ app.py βββββββββββββββββ
persona (browser) ββββΊ β /, /annuncio/3, /nuovo β HTML (Jinja2)β
β ββββΊ bacheca.db
programma ββββββββββββΊ β /api/annunci, /api/... β JSON β
(app, script, JS) ββββββββββββββββββββββββββββββββββββββββββ
Stessi dati, stesso database, stesse funzioni (trova_annuncio, valida_annuncio): cambia solo il formato della risposta.
REST: risorse e metodi
In un'API REST gli indirizzi indicano cose (le risorse), e il metodo HTTP dice cosa farci:
| Metodo | Indirizzo | Azione | Risposta |
|---|---|---|---|
GET |
/api/annunci |
elenco | 200 + lista |
GET |
/api/annunci?categoria=evento |
elenco filtrato | 200 + lista |
GET |
/api/annunci/3 |
un annuncio | 200, oppure 404 |
POST |
/api/annunci |
crea | 201 + l'annuncio creato, oppure 400 |
DELETE |
/api/annunci/3 |
elimina | 200, oppure 404 |
PUT |
/api/annunci/3 |
modifica | (missione 3) |
Notate: non ci sono indirizzi come /api/elimina_annuncio?id=3. L'indirizzo Γ¨ sempre la risorsa; l'azione la dice il metodo.
Leggere: GET
@app.route("/api/annunci")
def api_elenco():
categoria = request.args.get("categoria")
if categoria:
righe = leggi_tutti("SELECT * FROM annunci WHERE categoria = ? ORDER BY data DESC, id DESC", (categoria,))
else:
righe = leggi_tutti("SELECT * FROM annunci ORDER BY data DESC, id DESC")
return jsonify([dict(r) for r in righe])
Due cose nuove:
jsonify(...)trasforma liste e dizionari Python in testo JSON e aggiunge l'intestazioneContent-Type: application/json.dict(r): le righe disqlite3non sono "JSON-abili", quindi le trasformiamo in dizionari veri.[dict(r) for r in righe]lo fa per tutte.
@app.route("/api/annunci/<int:id>")
def api_annuncio(id):
a = trova_annuncio(id)
if a is None:
return jsonify({"errore": "annuncio non trovato"}), 404
return jsonify(dict(a))
Anche l'errore Γ¨ JSON, con il codice di stato giusto. Un programma non legge le frasi: controlla il codice. Per un'API, i codici sono importantissimi.
Creare: POST con JSON
@app.route("/api/annunci", methods=["POST"])
def api_crea():
dati = request.get_json(silent=True) # il JSON del corpo, oppure None
if dati is None:
return jsonify({"errore": "serve un corpo JSON"}), 400
titolo = str(dati.get("titolo", "")).strip()
testo = str(dati.get("testo", "")).strip()
categoria = dati.get("categoria", "avviso")
errori = valida_annuncio(titolo, testo, categoria) # la stessa del form!
if errori:
return jsonify({"errori": errori}), 400
nuovo_id = esegui("INSERT INTO annunci (titolo, testo, categoria) VALUES (?, ?, ?)",
(titolo, testo, categoria))
return jsonify(dict(trova_annuncio(nuovo_id))), 201
- Nel form i dati arrivavano in
request.form; in un'API arrivano come JSON e si leggono conrequest.get_json(). Consilent=True, se il corpo non Γ¨ JSON valido otteniamoNoneinvece di un errore. str(...): in JSON il client potrebbe mandare un numero o una lista al posto di una stringa. Convertendo,.strip()non fallisce.- La validazione Γ¨ una funzione sola,
valida_annuncio, usata sia dal form/nuovosia dall'API. Le regole sono scritte in un posto solo: se cambiano, cambiano ovunque. Guardate inapp.pycom'Γ¨ diventata la route/nuovo. - La risposta Γ¨ 201 Created con l'annuncio completo, compresi
idedatadecisi dal database.
| Codice | Significato | Nella bacheca |
|---|---|---|
| 200 | OK | lettura o eliminazione riuscita |
| 201 | Created | annuncio creato |
| 400 | Bad Request | JSON mancante o dati non validi |
| 404 | Not Found | annuncio inesistente |
| 405 | Method Not Allowed | metodo non previsto (es. PUT prima della missione 3) |
Provare l'API con Thunder Client
Il browser sa fare solo GET. Per gli altri metodi: icona del fulmine di Thunder Client in VS Code β New Request.
- GET
http://127.0.0.1:5000/api/annunciβ Send. A destra: il codice (200), il tempo, il JSON. - POST
http://127.0.0.1:5000/api/annunci, scheda Body β JSON:
{ "titolo": "Dal Thunder Client", "testo": "Prova dell'API", "categoria": "evento" }
- Rifate la POST con
"titolo": "X": risposta 400 con l'elenco degli errori. - DELETE
http://127.0.0.1:5000/api/annunci/1β 200; ripetetela β 404.
Un client in Python
Nel kit c'Γ¨ client.py: avviate la bacheca in un terminale e il client in un secondo terminale.
import requests
API = "http://127.0.0.1:5000/api/annunci"
r = requests.get(API, timeout=5)
for a in r.json(): # JSON β lista di dizionari
print(f"#{a['id']} [{a['categoria']}] {a['titolo']}")
r = requests.post(API, json={"titolo": "Dal client", "testo": "...", "categoria": "avviso"}, timeout=5)
print(r.status_code, r.json()) # 201 e l'annuncio creato
json=... trasforma il dizionario in JSON e imposta da solo l'intestazione Content-Type.
Un client nel browser: fetch
La pagina Live (templates/live.html + static/js/live.js) non riceve gli annunci da Jinja2: Γ¨ vuota, e li chiede all'API con JavaScript.
async function aggiorna() {
const risposta = await fetch("/api/annunci"); // richiesta GET all'API
const annunci = await risposta.json(); // JSON β array JavaScript
// ... per ogni annuncio crea una scheda nella pagina
}
aggiorna();
setInterval(aggiorna, 5000); // ripeti ogni 5 secondi
à lo stesso schema della chat di quarta. Nel codice del kit, i testi si inseriscono con textContent e non con innerHTML: così un annuncio con dentro del codice HTML viene mostrato come testo e non eseguito. Ricordate l'escape automatico di Jinja2? In JavaScript tocca a noi.
Per far partire il JavaScript solo nella pagina Live,
base.htmlha un nuovo blocco vuoto,{% block script %}{% endblock %}, prima del piè di pagina:live.htmllo riempie con il suo<script>.
Missioni
β Missione 1 β Il collaudo
Con Thunder Client eseguite e annotate in una tabella (richiesta, codice, risposta): l'elenco, l'elenco filtrato per compito, un annuncio esistente, uno inesistente, una POST corretta, una POST senza testo, una POST con "categoria": "pippo", una DELETE. Poi eseguite client.py.
ββ Missione 2 β Solo gli ultimi
Fate in modo che /api/annunci?limite=5 restituisca solo i 5 annunci piΓΉ recenti. Se limite non Γ¨ un numero, rispondete 400.
Aiutino
request.args.get("limite", type=int) restituisce giΓ un intero, oppure None se manca o non Γ¨ un numero. In SQL: ... LIMIT ?. Per distinguere "manca" da "sbagliato", controllate prima se "limite" in request.args.
βββ Missione 3 β Modificare: PUT
Aggiungete PUT /api/annunci/<id>: riceve un JSON con titolo, testo e categoria, li valida con valida_annuncio, esegue l'UPDATE e restituisce l'annuncio aggiornato (200). Se l'annuncio non esiste: 404.
βββ Missione 4 β Lo schermo dell'atrio
Create la pagina /schermo: sfondo scuro, scritte grandi, solo gli annunci di tipo evento, aggiornata ogni 10 secondi con fetch su /api/annunci?categoria=evento. Pensata per stare accesa su un televisore.
βββ Missione 5 β Il bot
Scrivete bot.py: ogni 30 secondi controlla l'API e, se trova annunci nuovi rispetto al controllo precedente, stampa "π Nuovo annuncio: β¦". Provatelo pubblicando dal sito mentre il bot gira.
Aiutino
Tenete un insieme (set) degli id giΓ visti. A ogni giro: nuovi = [a for a in annunci if a["id"] not in visti]. Per aspettare: import time e time.sleep(30).