Python-Kurs / Eine lokale Flask-JSON-API mit Eingabeprüfung erstellen
Eine kleine Flask-API verbindet HTTP-Anfragen mit Python-Funktionen. Prüfen Sie empfangene JSON-Objekte auf Pflichtfelder, Datentypen und zulässige Werte und unterscheiden Sie erfolgreiche Erstellung und Fehler durch passende Statuscodes.
Den lokalen Übungsrahmen und die Installation festlegen
Diese API ist eine Übung auf demselben Rechner. Ihre Daten liegen in einer Python-Liste, verschwinden beim Prozessende und werden nicht zwischen unabhängigen Serverprozessen geteilt. Ein tatsächlicher öffentlicher Dienst braucht zusätzliche Entscheidungen zu Datenbank, Anmeldung, Berechtigungen, Bereitstellung und gleichzeitigen Zugriffen.
Installieren Sie Flask in Ihrer Projektumgebung:
python -m pip install flask
Speichern Sie die Anwendung als app.py in einem neuen Übungsordner. Nennen Sie die Datei nicht flask.py, damit sie den gleichnamigen Bibliotheksimport nicht verdeckt. Starten Sie bei Bedarf python app.py. Der gezeigte Entwicklungsserver bindet ausschließlich an 127.0.0.1 und lässt den Debugger ausgeschaltet. Ist Port 5000 bereits belegt, ändern Sie Port und verwendete Client-Adresse zusammen, ohne einen fremden Dienst zu beenden.
Die Flask-Einführung beschreibt Routing und Entwicklungsbetrieb. Für die automatisierte Prüfung unten benötigen Sie keinen laufenden Serverprozess. Den Unterschied zwischen Datenformat und Speicherung erklärt der Kurs zu JSON.
Die vollständige API-Anwendung speichern
Die vollständige UTF-8-Datei app.py lautet:
from flask import Flask, jsonify, request
app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 4096
expenses = []
@app.errorhandler(413)
def too_large(error):
return jsonify(error="request body too large"), 413
@app.get("/api/expenses")
def list_expenses():
return jsonify(expenses)
@app.post("/api/expenses")
def add_expense():
data = request.get_json(silent=True)
if not isinstance(data, dict):
return jsonify(error="expected JSON object"), 400
name, amount = data.get("name"), data.get("amount")
if not isinstance(name, str) or not name.strip() or type(amount) is not int or amount < 0:
return jsonify(error="invalid name or amount"), 400
row = {"id": len(expenses) + 1, "name": name.strip(), "amount": amount}
expenses.append(row)
return jsonify(row), 201
if __name__ == "__main__":
app.run(host="127.0.0.1", port=5000, debug=False)
GET /api/expenses liefert den aktuellen Bestand als JSON-Liste mit Status 200. POST /api/expenses erwartet ein Objekt mit name und amount. Eine erfolgreiche Erstellung liefert Status 201 und den neu angelegten Datensatz. Bei einer ungültigen Eingabe wird Status 400 mit einem JSON-Fehlerobjekt zurückgegeben, ohne eine Zeile anzuhängen.
Ein Name muss eine nichtleere Zeichenfolge sein; beim Speichern wird äußerer Leerraum entfernt. Der Betrag muss vom genauen Python-Typ int und nichtnegativ sein. Die JSON-Werte true und false werden in Python zu bool. Da isinstance(True, int) wahr ist, verwendet das Beispiel bewusst type(amount) is not int. Eine Zeichenfolge wie "8000" oder ein Dezimalwert wird ebenfalls nicht als Ganzzahlbetrag angenommen.
request.get_json(silent=True) liefert bei ungeeigneten oder nicht lesbaren JSON-Eingaben hier kein passendes Dictionary; die Anwendung antwortet nach ihrer eigenen Regel mit expected JSON object. Diese kompakte Regel unterscheidet nicht jede einzelne Ursache wie fehlenden Inhaltstyp oder defekte JSON-Syntax in unterschiedlichen Fehlermeldungen.
MAX_CONTENT_LENGTH=4096 begrenzt den gelesenen Anfrageinhalt auf 4096 Bytes. Bei Überschreitung liefert der spezielle Handler ein JSON-Fehlerobjekt mit Status 413. Eine solche Einzelanfragegrenze ist noch keine Grenze für den gesamten gespeicherten Bestand: Die Liste kann durch viele gültige Anfragen wachsen. Höchstzahl, fachlicher Maximalbetrag, Persistenz und weitere Grenzen müssten separat festgelegt werden.
Die Routen mit dem Flask-Testclient prüfen
Speichern Sie die folgende vollständige Datei test_client.py neben app.py:
from app import app, expenses
expenses.clear()
with app.test_client() as client:
created = client.post("/api/expenses", json={"name":"lunch", "amount":8000})
assert created.status_code == 201
for payload in [{"name":"x","amount":True}, {"name":"x","amount":-1}, {"name":" ","amount":1}, [1,2]]:
response = client.post("/api/expenses", json=payload)
assert response.status_code == 400 and response.is_json
assert client.post("/api/expenses", data="{", content_type="application/json").status_code == 400
too_large = client.post("/api/expenses", data="x"*5000, content_type="application/json")
assert too_large.status_code == 413 and too_large.is_json
rows = client.get("/api/expenses")
assert rows.status_code == 200 and len(rows.get_json()) == 1
print(created.status_code, rows.get_json()[0]["amount"])
print("validation and 413 checks passed")
Führen Sie im gleichen Ordner und derselben Umgebung aus:
python test_client.py
Erwartet werden zwei Ausgabezeilen: 201 8000 und validation and 413 checks passed. Der Test leert zuerst die Liste des importierten Übungsmoduls, erstellt eine gültige Ausgabe und prüft mehrere Ablehnungen. Am Ende darf genau die eine gültige Ausgabe gespeichert sein.
Der Testclient ruft die Anwendungslogik ohne echten Netzwerkserver auf. Ein gesonderter Prozess python app.py besitzt einen eigenen Speicher und wird durch expenses.clear() dieses Testprozesses nicht geleert. Verwechseln Sie daher das Ergebnis des Testclients nicht mit dem Zustand eines daneben gestarteten Servers. Die Flask-Dokumentation zum Testen erläutert diesen Ansatz.
Starten Sie den gezeigten Prüfhelfer ohne -O und ohne PYTHONOPTIMIZE, damit die normalen assert-Prüfungen ausgeführt werden. Der Test prüft das Routing, Statuscodes und JSON-Ergebnisse dieser Anwendung. Er ist keine Bestätigung von Netzwerkverbindung, Reverse Proxy oder produktiver Bereitstellung.
Fehlervertrag und Lebensdauer richtig einordnen
Die ausdrücklich behandelten Validierungsfehler liefern JSON mit Status 400; der spezielle Größenfehler liefert JSON mit 413. Daraus folgt nicht, dass jede mögliche Fehlerantwort JSON ist. Ein unbekannter Pfad oder eine nicht unterstützte Methode kann weiterhin eine normale HTML-Fehlerseite erzeugen. Auch ein vorgeschalteter Server oder Proxy kann eine Anfrage vor Flask zurückweisen. Die Flask-Konfiguration zur Anfragegröße beschreibt die Grenze.
Die Kennung entsteht aus der Listenlänge plus eins. Für diese kleine kontrollierte Demonstration genügt das, ist aber kein Modell für dauerhaft eindeutige Kennungen bei parallelen Prozessen, Löschungen oder Neustarts. Bei erneutem gleichen POST wird außerdem eine neue Zeile erzeugt; es gibt keinen Auftragsschlüssel, der Wiederholungen zusammenführt.
- Vergleichen Sie einen gültigen Betrag mit booleschem Wert, negativem Betrag und Zahlen als Text. Nur die gültige Eingabe darf gespeichert werden.
- Prüfen Sie einen leeren Namen, ein Array statt eines Objekts und beschädigtes JSON. Vergleichen Sie Status, JSON-Fehlertext und unveränderte Datensatzanzahl.
- Prüfen Sie die zu große Anfrage aus dem Test und die definierte 413-Antwort. Ein normales unbekanntes Routingziel hat dagegen einen anderen Fehlervertrag.
- Wenn Sie den lokalen Server separat gestartet haben, beenden Sie ihn nach der Übung mit Ctrl+C. Nach einem Neustart beginnt dessen Liste wieder leer.
Stellen Sie den Entwicklungsserver und insbesondere einen interaktiven Debugger nicht unverändert als öffentlichen Dienst bereit. Für dauerhafte Transaktionsdaten verbinden Sie eine passende Speicherung und Transaktionsregeln, wie im SQLite-Kurs beschrieben. Prüfen Sie eine spätere Bereitstellung gesondert, statt erfolgreiche lokale Anwendungstests auf ungeprüfte Infrastruktur zu übertragen.




