Abschnitt 18 · Lektion 6
Eine JSON-Schnittstelle
Ein Programm, das wissen will, was im September für Lebensmittel draufging, müsste die Zahl aus dem HTML des Haushaltsbuchs herausklauben. Bisher schickt der Server nur Seiten für Menschen, HTML für den Browser. Viel einfacher ist es, dem Programm gleich Daten zu schicken.
In Abschnitt 17 hat dein Programm einen Dienst gefragt und Daten als JSON bekommen. Jetzt geht es um die andere Seite, einen Server, an dem du jede Zeile selbst geschrieben hast.
Ein Dictionary wird JSON
# Die Ausgaben des Haushaltsbuchs für diesen Abschnitt, dazu zwei Helfer.
# Hier musst du nichts ändern.
from decimal import Decimal, InvalidOperation
# Alle Kategorien, auch die, in denen noch nichts steht.
KATEGORIEN = ["Lebensmittel", "Wohnen", "Mobilität", "Freizeit", "Haushalt", "Kleidung", "Sonstiges"]
# Die Ausgaben im September. Der Betrag steht in Cent wie in Lektion 16.2, 840
# sind 8,40 €. Beim Flohmarkt hat jemand <b> mit eingetippt, damit der Name fett
# dasteht.
AUSGABEN = [
{"empfaenger": "Verkehrsbetriebe Lindenau", "kategorie": "Mobilität", "betrag": 5800},
{"empfaenger": "Supermarkt Sonnenblick", "kategorie": "Lebensmittel", "betrag": 5218},
{"empfaenger": "Bäckerei Krume", "kategorie": "Lebensmittel", "betrag": 840},
{"empfaenger": "Pizzeria L'Angolo", "kategorie": "Freizeit", "betrag": 3450},
{"empfaenger": "Bäckerei Krume", "kategorie": "Lebensmittel", "betrag": 410},
{"empfaenger": "Kino Lichtspiel", "kategorie": "Freizeit", "betrag": 2400},
{"empfaenger": "Hausverwaltung Ahorn", "kategorie": "Wohnen", "betrag": 112000},
{"empfaenger": "Drogerie Seifenblase", "kategorie": "Haushalt", "betrag": 1295},
{"empfaenger": "Supermarkt Sonnenblick", "kategorie": "Lebensmittel", "betrag": 5000},
{"empfaenger": "Pizzeria L'Angolo", "kategorie": "Freizeit", "betrag": 6130},
{"empfaenger": "<b>Flohmarkt</b>", "kategorie": "Freizeit", "betrag": 1500},
{"empfaenger": "Supermarkt Sonnenblick", "kategorie": "Lebensmittel", "betrag": 6347},
{"empfaenger": "Stadtwerke Lindenau", "kategorie": "Wohnen", "betrag": 8400},
]
def euro(cent):
"""Schreibt einen Betrag in Cent auf Deutsch: aus 112000 wird "1.120,00 €"."""
vorzeichen = "-" if cent < 0 else ""
ganze, rest = divmod(abs(cent), 100)
return vorzeichen + f"{ganze:,}".replace(",", ".") + f",{rest:02d} €"
def cent_aus_text(text):
"""Liest einen Betrag wie "3,20" oder "1.120,00" und gibt ihn in Cent zurück.
Das Komma trennt die Cent ab, ein Punkt die Tausender. Ist der Text kein
Betrag mit höchstens zwei Stellen nach dem Komma, gibt es einen ValueError.
"""
try:
betrag = Decimal(text.strip().replace(".", "").replace(",", "."))
except InvalidOperation:
raise ValueError(f"Kein Betrag: {text!r}") from None
if not betrag.is_finite() or betrag != round(betrag, 2):
raise ValueError(f"Kein Betrag: {text!r}")
return int(betrag * 100) from flask import Flask
from haushalt import AUSGABEN, KATEGORIEN
app = Flask(__name__)
@app.get("/api/kategorien")
def kategorien():
return {"kategorien": KATEGORIEN}
@app.get("/api/ausgabe/<int:nummer>")
def ausgabe(nummer):
return AUSGABEN[nummer - 1] Die Route gibt ein Dictionary zurück. Das ist schon alles. Flask macht daraus JSON und schickt es
mit der Kopfzeile Content-Type: application/json, wie der Wetterdienst. Im Reiter Browser
steht nur der Text, mehr ist damit dort nicht anzufangen. Mit dem ersten Befehl in der Befehlsleiste
siehst du die Antwort samt Kopfzeilen.
Eine Liste geht genauso, /api/kategorien gibt eine Liste in einem Dictionary zurück. Damit ist
dein Haushaltsbuch eine Schnittstelle wie die aus Abschnitt 17.
Am JSON fallen zwei Dinge auf. Die Schlüssel stehen alphabetisch, betrag vor empfaenger, obwohl
sie im Dictionary andersherum stehen. Flask sortiert sie, damit dieselben Daten immer gleich
aussehen. Und aus „Bäckerei“ ist B\u00e4ckerei geworden, wie bei json.dumps ohne
ensure_ascii=False in Lektion 12.5.
Beides ist kein Fehler. Wenn ein Programm die Antwort mit json() liest, bekommt es wieder ein
Dictionary mit „Bäckerei“ darin, und auf die Reihenfolge kommt es in einem Dictionary sowieso nicht
an.
Der Betrag steht in Cent da, 840, wie in der Datenbank aus Abschnitt 16. Viele Schnittstellen im
echten Leben machen das auch so, etwa die von Zahlungsdiensten, damit kein Programm am anderen Ende
mit Kommazahlen rechnen muss. Was die Zahl bedeutet, steht dann in der Beschreibung der
Schnittstelle.
Unter /api/ausgabe/14 wird die Nummer ungeprüft verwendet, und es gibt einen Absturz mit 500,
genau wie vorher bei der Seite für eine einzelne Ausgabe.
Der Status gehört dazu
# Die Ausgaben des Haushaltsbuchs für diesen Abschnitt, dazu zwei Helfer.
# Hier musst du nichts ändern.
from decimal import Decimal, InvalidOperation
# Alle Kategorien, auch die, in denen noch nichts steht.
KATEGORIEN = ["Lebensmittel", "Wohnen", "Mobilität", "Freizeit", "Haushalt", "Kleidung", "Sonstiges"]
# Die Ausgaben im September. Der Betrag steht in Cent wie in Lektion 16.2, 840
# sind 8,40 €. Beim Flohmarkt hat jemand <b> mit eingetippt, damit der Name fett
# dasteht.
AUSGABEN = [
{"empfaenger": "Verkehrsbetriebe Lindenau", "kategorie": "Mobilität", "betrag": 5800},
{"empfaenger": "Supermarkt Sonnenblick", "kategorie": "Lebensmittel", "betrag": 5218},
{"empfaenger": "Bäckerei Krume", "kategorie": "Lebensmittel", "betrag": 840},
{"empfaenger": "Pizzeria L'Angolo", "kategorie": "Freizeit", "betrag": 3450},
{"empfaenger": "Bäckerei Krume", "kategorie": "Lebensmittel", "betrag": 410},
{"empfaenger": "Kino Lichtspiel", "kategorie": "Freizeit", "betrag": 2400},
{"empfaenger": "Hausverwaltung Ahorn", "kategorie": "Wohnen", "betrag": 112000},
{"empfaenger": "Drogerie Seifenblase", "kategorie": "Haushalt", "betrag": 1295},
{"empfaenger": "Supermarkt Sonnenblick", "kategorie": "Lebensmittel", "betrag": 5000},
{"empfaenger": "Pizzeria L'Angolo", "kategorie": "Freizeit", "betrag": 6130},
{"empfaenger": "<b>Flohmarkt</b>", "kategorie": "Freizeit", "betrag": 1500},
{"empfaenger": "Supermarkt Sonnenblick", "kategorie": "Lebensmittel", "betrag": 6347},
{"empfaenger": "Stadtwerke Lindenau", "kategorie": "Wohnen", "betrag": 8400},
]
def euro(cent):
"""Schreibt einen Betrag in Cent auf Deutsch: aus 112000 wird "1.120,00 €"."""
vorzeichen = "-" if cent < 0 else ""
ganze, rest = divmod(abs(cent), 100)
return vorzeichen + f"{ganze:,}".replace(",", ".") + f",{rest:02d} €"
def cent_aus_text(text):
"""Liest einen Betrag wie "3,20" oder "1.120,00" und gibt ihn in Cent zurück.
Das Komma trennt die Cent ab, ein Punkt die Tausender. Ist der Text kein
Betrag mit höchstens zwei Stellen nach dem Komma, gibt es einen ValueError.
"""
try:
betrag = Decimal(text.strip().replace(".", "").replace(",", "."))
except InvalidOperation:
raise ValueError(f"Kein Betrag: {text!r}") from None
if not betrag.is_finite() or betrag != round(betrag, 2):
raise ValueError(f"Kein Betrag: {text!r}")
return int(betrag * 100) from flask import Flask
from haushalt import AUSGABEN
app = Flask(__name__)
@app.get("/api/ausgabe/<int:nummer>")
def ausgabe(nummer):
if nummer < 1 or nummer > len(AUSGABEN):
return {"fehler": f"Keine Ausgabe mit der Nummer {nummer}"}, 404
return AUSGABEN[nummer - 1] Bei einer Seite für Menschen war abort(404) die ehrliche Antwort. Eine Schnittstelle antwortet
aber auch im Fehlerfall mit JSON. Sonst bekommt das Programm am anderen Ende eine HTML-Seite, die es
nicht lesen kann.
return {"fehler": f"Keine Ausgabe mit der Nummer {nummer}"}, 404 gibt beides zurück, die Daten und
den Statuscode, so wie render_template(…), 400 beim Formular.
Mit return {"fehler": …} allein käme die Meldung mit 200 an, also „alles in Ordnung“. Ein
Programm, das sich am Status orientiert, würde danach im Dictionary nach einem Betrag greifen, den
es nicht gibt. Wer eine Schnittstelle baut, setzt den Status deshalb bewusst. 404, wenn es etwas
nicht gibt, 400, wenn die Anfrage nicht stimmt, und 200, wenn alles passt. Das ist der Normalfall
und muss nicht dastehen.
Die andere Seite
# Die Ausgaben des Haushaltsbuchs für diesen Abschnitt, dazu zwei Helfer.
# Hier musst du nichts ändern.
from decimal import Decimal, InvalidOperation
# Alle Kategorien, auch die, in denen noch nichts steht.
KATEGORIEN = ["Lebensmittel", "Wohnen", "Mobilität", "Freizeit", "Haushalt", "Kleidung", "Sonstiges"]
# Die Ausgaben im September. Der Betrag steht in Cent wie in Lektion 16.2, 840
# sind 8,40 €. Beim Flohmarkt hat jemand <b> mit eingetippt, damit der Name fett
# dasteht.
AUSGABEN = [
{"empfaenger": "Verkehrsbetriebe Lindenau", "kategorie": "Mobilität", "betrag": 5800},
{"empfaenger": "Supermarkt Sonnenblick", "kategorie": "Lebensmittel", "betrag": 5218},
{"empfaenger": "Bäckerei Krume", "kategorie": "Lebensmittel", "betrag": 840},
{"empfaenger": "Pizzeria L'Angolo", "kategorie": "Freizeit", "betrag": 3450},
{"empfaenger": "Bäckerei Krume", "kategorie": "Lebensmittel", "betrag": 410},
{"empfaenger": "Kino Lichtspiel", "kategorie": "Freizeit", "betrag": 2400},
{"empfaenger": "Hausverwaltung Ahorn", "kategorie": "Wohnen", "betrag": 112000},
{"empfaenger": "Drogerie Seifenblase", "kategorie": "Haushalt", "betrag": 1295},
{"empfaenger": "Supermarkt Sonnenblick", "kategorie": "Lebensmittel", "betrag": 5000},
{"empfaenger": "Pizzeria L'Angolo", "kategorie": "Freizeit", "betrag": 6130},
{"empfaenger": "<b>Flohmarkt</b>", "kategorie": "Freizeit", "betrag": 1500},
{"empfaenger": "Supermarkt Sonnenblick", "kategorie": "Lebensmittel", "betrag": 6347},
{"empfaenger": "Stadtwerke Lindenau", "kategorie": "Wohnen", "betrag": 8400},
]
def euro(cent):
"""Schreibt einen Betrag in Cent auf Deutsch: aus 112000 wird "1.120,00 €"."""
vorzeichen = "-" if cent < 0 else ""
ganze, rest = divmod(abs(cent), 100)
return vorzeichen + f"{ganze:,}".replace(",", ".") + f",{rest:02d} €"
def cent_aus_text(text):
"""Liest einen Betrag wie "3,20" oder "1.120,00" und gibt ihn in Cent zurück.
Das Komma trennt die Cent ab, ein Punkt die Tausender. Ist der Text kein
Betrag mit höchstens zwei Stellen nach dem Komma, gibt es einen ValueError.
"""
try:
betrag = Decimal(text.strip().replace(".", "").replace(",", "."))
except InvalidOperation:
raise ValueError(f"Kein Betrag: {text!r}") from None
if not betrag.is_finite() or betrag != round(betrag, 2):
raise ValueError(f"Kein Betrag: {text!r}")
return int(betrag * 100) from flask import Flask
from haushalt import AUSGABEN
app = Flask(__name__)
@app.get("/api/ausgabe/<int:nummer>")
def ausgabe(nummer):
if nummer < 1 or nummer > len(AUSGABEN):
return {"fehler": f"Keine Ausgabe mit der Nummer {nummer}"}, 404
return AUSGABEN[nummer - 1] # Ein zweites Programm, das deine Anwendung fragt, so wie die Programme in
# Abschnitt 17 den Wetterdienst gefragt haben. Es läuft im Terminal, während
# die Anwendung läuft.
import requests
ADRESSE = "http://127.0.0.1:3000"
antwort = requests.get(ADRESSE + "/api/ausgabe/3", timeout=2)
print(antwort.status_code, antwort.headers["Content-Type"])
print(antwort.json())
antwort = requests.get(ADRESSE + "/api/ausgabe/14", timeout=2)
print(antwort.status_code, antwort.json()) Hier schließt sich der Kreis zu Abschnitt 17. In der Befehlsleiste steht python frage.py. Wenn die
Anwendung noch nicht läuft, wird sie vorher gestartet, wie bei Sandbox starten.
frage.py ist ein zweites Programm, gebaut wie die aus Abschnitt 17, und es fragt deine Anwendung
über requests, während sie läuft. Für die dritte Ausgabe kommen der Status 200, der Typ
application/json und ein Dictionary mit „Bäckerei Krume“ zurück, geschrieben mit ä. Für die
vierzehnte kommen 404 und die Meldung. Was in Abschnitt 17 der Wetterdienst war, ist jetzt deine
eigene Anwendung.
Zu Hause brauchst du dafür zwei Terminals. In einem läuft der Server und wartet, im anderen startest
du python frage.py. Ohne den Server endet es mit dem ConnectionError aus Lektion 17.1, weil an
dem Port dann niemand zuhört. Wenn du Flask zu Hause ohne --port startest, ist es Port 5000, und
in frage.py muss dann auch 5000 stehen.
Eine Menge ist kein Dictionary
from flask import Flask
app = Flask(__name__)
@app.get("/api/kategorien")
def kategorien():
return {"Lebensmittel", "Wohnen", "Freizeit"} Die geschweiften Klammern sehen aus wie ein Dictionary. Es fehlen aber die Doppelpunkte, und damit ist es eine Menge. Damit kann Flask nichts anfangen. Im Browser steht ein Traceback, der so endet:
TypeError: The view function did not return a valid response. The return type must be a string, dict, list, tuple with headers or status, Response instance, or WSGI callable, but it was a set.
Übersetzt heißt das: Die Funktion hat keine gültige Antwort zurückgegeben. Erlaubt sind Text, ein Dictionary, eine Liste, ein Tupel mit Status oder Kopfzeilen und zwei Dinge, die hier nicht vorkommen, es war aber eine Menge (set). Richtig wäre eine Liste in eckigen Klammern oder ein Dictionary mit Schlüssel und Wert.
Was die Aufgabe will
Gefragt sind die Summen je Kategorie, diesmal als Daten. Eine Adresse liefert alle Kategorien, eine zweite eine einzelne. Wenn es die Kategorie nicht gibt, kommt eine ehrliche 404 als JSON.
Zum Mitnehmen
Wenn eine Route ein Dictionary oder eine Liste zurückgibt, macht Flask JSON daraus und schickt es mit dem Typ application/json. Mit return {"fehler": "…"}, 404 kommt der Statuscode bewusst dazu, damit ein Programm am anderen Ende weiß, woran es ist. Dass Flask die Schlüssel sortiert und Umlaute als \u00e4 schreibt, macht nichts, requests liest beides wieder richtig.
Jetzt du
Basis Konto, kostenlosZu dieser Lektion gehört eine Aufgabe. Du schreibst den Code selbst, und nach jedem Lauf sagt dir eine Prüfliste, was schon stimmt.
Dafür brauchst du das Basis Konto. Es kostet nichts, und ein Passwort gibt es auch nicht.
In diesem Kurs läuft dein Code auf einem Server. Dafür hat das Basis Konto 1 Stunde im Monat, mehr Zeit gibt es mit dem Premium Konto.
Was in dieser Lektion steckt
-
Artikel mit 4 Beispielen zum Ausprobieren
Steht hier, ohne Konto lesbar.
-
Aufgabe, dein Code läuft auf einem Server
Öffnet sich mit dem Basis Konto.