Was ein Framework für dich tut · Übung
Lass ein Modell den Body prüfen
In Kapitel 5 haben zwei reading_server.py. body_error hat sich die Anfrage angesehen und book_error die geparsten Daten:
| Funktion | Lehnt ab, wenn | Status |
|---|---|---|
body_error(request) | content-type nicht application/json ist | 415 |
body_error(request) | der Body kein | 400 |
book_error(data) | kein JSON- | 422 |
book_error(data) | Titel oder Autor fehlt | 422 |
book_error(data) | Titel oder Autor kein nicht leerer | 422 |
handle hat sie in dieser Reihenfolge aufgerufen, und create_book und replace_book liefen nur, wenn beide None zurückgaben. Die App erledigt beide Aufgaben mit dem Modell, das du in Lektion 1 gelesen hast, BookIn, und einer Annotation in jeder def-Zeile: book: BookIn.
Das Modell prüft zuerst
Wenn POST /books ankommt, liest BookIn, alles bevor create_book läuft. Deine Funktion wird nur mit einem Body aufgerufen, der die Prüfung bestanden hat, und sie bekommt diesen Body als BookIn-Objekt. Das Objekt gibt seine geprüften book.title und book.author. Es ist kein book["title"]
Mit den Versionen, die dieser Kurs benutzt, lehnt das Modell Folgendes ab:
einen Body, der kein JSON-Objekt ist oder dem
titleoderauthorfehlt;ein Feld, das kein String ist:
7undtruewerden abgelehnt, nie in"7"oder"true"umgewandelt;ein Feld ohne ein anderes Zeichen als Leerzeichen, etwa
""oder" ". Das verlangtpattern=r"\S": ein Zeichen, irgendwo, das kein Leerzeichen, kein Tab, kein Zeilenumbruch und keine andere Art von Leerraum ist.
Den Rest akzeptiert es so, wie er ist. " Ærø " behält seine Leerzeichen, "7" ist ein String, und ein Titel mit 3000 Zeichen hat keine Grenze, die er überschreiten könnte. Felder, die das Modell nicht nennt, etwa "id": 99, werden verworfen, bevor deine Funktion den Body sieht, also kann ein Client die ID eines Buchs nicht wählen.
Ein POST-Body ist {"title": "Dune"}. Wann läuft create_book?
Die 422 von FastAPI lesen
Auf diesen Body hat dein Server aus Kapitel 5 mit {"error": "author is missing"} geantwortet. Die App antwortet mit 422 und diesem Body:
{"detail":[{"type":"missing","loc":["body","author"],"msg":"Field required","input":{"title":"Dune"}}]}
detail ist eine type die Regel, die verletzt wurde (hier missing; string_type für eine Zahl, wo ein String hingehört; string_pattern_mismatch für einen leeren Titel), loc sagt, wo (["body", "author"], das Feld author des Bodys), msg sagt es in Worten, und input gibt den geprüften Wert wieder. Dein Fehler war ein Satz für einen Menschen. Dieser hier sind Daten, mit denen ein Client-Programm Feld für Feld etwas anfangen kann. Beide tragen 422.
Wo das Framework anders entscheidet
Die meisten fehlerhaften Bodys bekommen denselben Status wie von deinem Server. Diese nicht:
| Anfrage | Kapitel 5 | Die FastAPI-App |
|---|---|---|
ein gültiges JSON-Buch, als text/plain gekennzeichnet | 415 | 422 |
ein gültiges JSON-Buch ohne content-type | 415 | 422 |
| JSON, das sich nicht parsen lässt | 400 | 422 |
| Bytes, die kein UTF-8 sind | 400 | 400, {"detail":"There was an error parsing the body"} |
ein Buch, als application/merge-patch+json gekennzeichnet | 415 | angenommen, 201 |
PUT /books/9999, kein solches Buch, mit kaputtem Body | 404 | 422 |
FastAPI parst einen Body nur dann als JSON, wenn sein content-type JSON sagt: application/json oder einen application/-+json endet, etwa application/merge-patch+json. Jede andere Kennzeichnung, oder gar keine, lässt den Body als Text, und das Modell lehnt Text ab, weil er kein Objekt ist; die 422 sagt, dass das Modell ein Objekt erwartet hat. Eine eigene 415 gibt es nicht. Kaputtes JSON ist ebenfalls eine 422, mit dem Typ json_invalid. Bytes, die sich nicht als UTF-8 decodieren lassen, sind das einzige Body-Problem, das FastAPI mit 400 beantwortet, und sein detail ist dort ein einfacher String.
Bei der PUT-Zeile geht es um die Reihenfolge. Dein Server hat die ID nachgeschlagen, bevor er den Body gelesen hat, also war eine unbekannte ID eine 404, egal was der Body enthielt. FastAPI prüft den Body, bevor es replace_book aufruft, also ist ein fehlerhafter Body eine 422, bevor deine Funktion irgendetwas nachschlagen kann. Ab Kapitel 7 hat dein Server außerdem vor beidem nach einem Token gefragt; diese App hat keine Tokens.
Zwei read_body; TestClient sendet immer eine Länge, und die App setzt keine Größengrenze.
Ein gültiges JSON-Buch ist als text/plain gekennzeichnet. Kapitel 5 hat mit 415 geantwortet. Was antwortet die App?
PUT /books/9999, wobei kein Buch 9999 existiert, enthält einen kaputten Body. Kapitel 5 hat mit 404 geantwortet. Was antwortet die App?
201 und location
Die Zeile über create_book lautet @app.post("/books", status_code=201): Jeder Wert, den die Funktion zurückgibt, geht mit dem Status 201 hinaus. Die def-Zeile fragt außerdem nach response: Response. FastAPI übergibt ein Antwortobjekt, und jeder Header, den du darauf setzt, wird in die Antwort kopiert:
response.headers["location"] = f"/books/{book_id}"
Gib das gespeicherte Buch zurück, und FastAPI sendet es als JSON-Body der 201, mit deiner location. replace_book braucht beides nicht: Ein ersetztes Buch ist eine einfache 200.
Deine Aufgabe
Schreibe die Körper von create_book und replace_book in reading_app.py:
create_book(book, response)nimmt die ID ausSTATE["next_id"]und erhöhtSTATE["next_id"]um eins, speichert{"id": ..., "title": book.title, "author": book.author}unter dieser ID inSTATE["books"], setztlocationauf/books/und die ID und gibt das gespeicherte Buch zurück.replace_book(book_id, book)löst für eine ID, dieSTATE["books"]nicht enthält,HTTPException(status_code=404, detail="no such book")aus. Sonst speichert es das Buch unterbook_id, der ID aus der, und gibt es zurück.
Schreibe keine eigenen Prüfungen für title oder author: Das hat das Modell erledigt, und eine Regel von dir, die etwas ablehnt, das das Modell akzeptiert, etwa einen Titel mit Leerzeichen drumherum, würde die App mit ihrem eigenen Modell uneins machen. So probierst du eine Anfrage im Tab Shell aus, mit abgeschalteter Import-Warnung des Test-Clients:
python -W ignore -c "from fastapi.testclient import TestClient; import reading_app; r = TestClient(reading_app.app).post('/books', json={'title': 'Piranesi', 'author': 'Susanna Clarke'}); print(r.status_code, r.headers['location'], r.json())"
Es gibt 201 /books/3 {'id': 3, 'title': 'Piranesi', 'author': 'Susanna Clarke'} aus. Klicke dann auf Check my work. Bisher hat kein Byte der Antworten deiner App einen
Aufgabe
Schreibe die Körper von create_book und replace_book in reading_app.py, unter die @app.post- und @app.put-Zeilen, die schon da sind.
create_book(book, response)nimmt die neue ID ausSTATE["next_id"]und erhöhtSTATE["next_id"]um eins. Es speichert{"id": ..., "title": book.title, "author": book.author}unter dieser ID inSTATE["books"], setztresponse.headers["location"]auf/books/, gefolgt von der ID, und gibt das gespeicherte Buch zurück. Das mitgeliefertestatus_code=201macht die Antwort zu einer 201.replace_book(book_id, book)löstHTTPException(status_code=404, detail="no such book")aus, wennSTATE["books"]kein Buch mit dieser ID hat. Sonst speichert es{"id": book_id, "title": book.title, "author": book.author}unterbook_id, der ID aus der, und gibt es zurück.
book ist ein BookIn, das book.title und book.author und füge keine eigenen Prüfungen dafür hinzu. Behalte jede @app-Zeile, jede def-Zeile und BookIn so, wie sie gekommen sind.
Im python -m pytest -q weiterhin deine Tests aus, und der einzeilige Befehl python -W ignore -c ... in der Lektion sendet ein POST.
Klicke dann auf Check my work. Es füllt STATE über reset_state mit Büchern, die du noch nicht gesehen hast, und einer next_id von 41 und sendet dann mit TestClient POST- und PUT-Anfragen an die App in diesem Prozess, mit Titeln und Autoren, die du noch nicht gesehen hast. Ein neues Buch muss eine 201 bekommen, eine location, die seine ID nennt, und das gespeicherte Buch im Body, und next_id muss weiterrücken; ein PUT muss die ID aus der URL behalten; ein PUT an eine unbekannte ID muss eine 404 bekommen. Bodys, die das Modell ablehnt, müssen die 422 von FastAPI bekommen, oder dessen 400 für Bytes, die kein BookIn-