Kapitel 11 · Übung
Was ein Framework für dich tut
Dein Handler als Path Operations
Hier ist der Teil deines handle aus Kapitel 9, der /books/{id} beantwortet, aus der reading_server.py in den Dateien dieses Kapitels, ohne die Zweige für Schreibzugriffe:
elif len(segments) == 2 and segments[0] == "books":
book_id = book_id_from(segments[1])
...
elif book_id is None or book_id not in state["books"]:
response = json_error(404, "no book has that id")
elif method == "GET" or method == "HEAD":
response = add_validators(json_response(200, state["books"][book_id]))
...
else:
response = not_allowed("GET, HEAD, PUT, DELETE")
...
if method == "HEAD":
response["body"] = b""
Lies ihn als zwei Arten von Zeilen. Ein paar Zeilen entscheiden etwas über die Leseliste: Eine ID benennt nur dann ein Buch, wenn der Zustand es enthält, ein unbekanntes Buch ist eine 404, und ein gefundenes Buch ist die Antwort. Der Rest ist Klempnerarbeit. path_segments hat das Ziel vor diesem Zweig geteilt und decodiert, und die Vergleiche mit segments haben die Anfrage hierher geroutet. book_id_from macht aus Text einen int. json_response codiert das content-length. not_allowed lehnt andere allow-HEAD. Du hast jede davon geschrieben, also weißt du, was eine Antwort enthält.
Ein Framework übernimmt die Klempnerarbeit und überlässt dir die Entscheidungen. Dieses Kapitel schreibt die Leseliste mit reading_app.py. Dein Server aus Kapitel 9 bleibt unverändert daneben stehen, zum Vergleich. Die App behält die fünf Routen aus Kapitel 5: GET und POST auf /books und GET, PUT und DELETE auf /books/{id}. Login, Tokens,
Den Decorator lesen
Öffne reading_app.py. Weit oben erstellt app = FastAPI() das Anwendungsobjekt. Weiter unten stehen fünf NotImplementedError auslösen, jede mit einer Zeile darüber, die mit @ beginnt:
@app.get("/books/{book_id}")
def get_book(book_id: int):
"""Return one book, or raise HTTPException with status 404."""
@app.get("/books/{book_id}") registriert die Funktion darunter bei der App, für GET-Anfragen, deren Pfad zu /books/{book_id} passt. Wenn eine solche Anfrage ankommt, ruft FastAPI get_book auf. Das Registrieren ruft nichts auf, und die Funktion selbst bleibt eine gewöhnliche Funktion: Sobald du sie geschrieben hast, funktioniert reading_app.get_book(1), wenn du sie selbst aufrufst, ganz ohne Anfrage.
Eine Zeile, die mit @ beginnt und über einem def steht, ist ein Decorator. Python Fundamentals II behandelt Decorators, Klassen und Annotationen ausführlich. Dieser Kurs verlangt nie, dass du eines davon schreibst: Jede @app-Zeile, die das Kapitel braucht, steht schon in der Datei, und du schreibst nur Funktionskörper.
Was tut @app.get("/books/{book_id}") über def get_book(book_id: int):?
Annotationen, die FastAPI liest
Das {book_id} im Pfad benennt einen Teil der book_id: int in der def-Zeile verbindet ihn mit dem request: dict eine Notiz, die Python ignoriert. Diese hier liest FastAPI: Es nimmt das Pfadsegment, macht vor dem Aufruf deiner Funktion einen int daraus und antwortet selbst mit 422, wenn das nicht geht. Innerhalb von get_book ist book_id also immer ein int. Dein Server hat für /books/abc mit 404 geantwortet, weil book_id_from None zurückgegeben hat; diese App antwortet mit 422.
Dieselbe Datei enthält ein Modell, eine Klasse, die Daten beschreibt:
class BookIn(BaseModel):
"""The JSON object a POST or PUT body must be."""
title: str = Field(pattern=r"\S")
author: str = Field(pattern=r"\S")
Es beschreibt das JSON-title und author list_books hat einen annotierten Parameter, limit, den Lektion 5 benutzt; lass ihn vorerst in Ruhe.
Eine Anfrage fragt nach /books/abc. Wer antwortet?
Was deine Funktion zurückgibt
Eine Funktion, die so registriert ist, heißt Path Operation, und sie antwortet, indem sie einen None zurück, und FastAPI codiert es als JSON (aus None wird null), setzt content-type: application/json und eine content-length, die die Bytes zählt, und sendet den Status 200. Für GET /books gibst du {"books": [...]} mit den Büchern in der Reihenfolge ihrer IDs zurück, die Form, die dein Server gesendet hat.
Für eine unbekannte ID löst die Funktion eine
raise HTTPException(status_code=404, detail="no such book")
FastAPI sendet eine 404, deren JSON-Body {"detail":"no such book"} ist. Wo FastAPI detail sagt, hat dein Server error gesagt. FastAPI schreibt JSON außerdem ohne Leerzeichen nach : und ,, also brauchen dieselben Bücher weniger Bytes, als dein Server gesendet hat. Lektion 4 zeigt diese Bytes.
Wie antwortet get_book für eine unbekannte ID mit 404?
Wo die Bücher leben
get_book(book_id) hat keinen Parameter state, weil FastAPI einer Funktion nur die Teile der Anfrage übergibt, nach denen sie fragt. Die Bücher leben in einem Dictionary oben im STATE, mit der Form des Zustands deines Servers: STATE["books"] ordnet IDs Bücher zu, und STATE["next_id"] ist die nächste ID. reset_state(state) ersetzt seinen Inhalt durch den Zustand, den du übergibst, oder durch die zwei Beispielbücher, und jeder Test ruft es zuerst auf, damit kein Test Bücher sieht, die ein anderer Test hinzugefügt hat.
Was verschwunden ist
Deine zwei Funktionen werden je etwa vier Zeilen lang. Aus deinem Code verschwunden sind:
path_segmentsund die Vergleiche mitsegments: Die Pfade in den@app.get-Zeilen routen jede Anfrage.book_id_from:book_id: intwandelt die ID um.json_responseund seine Byte-Zählung: FastAPI codiert das Dictionary und setztcontent-length.die
not_allowed-Zweige: FastAPI antwortet selbst mit 405, wenn keine Route für einen Pfad die Methode akzeptiert.die HEAD-Regel: Nichts in der App erwähnt
HEAD. Lektion 2 fragt die App, was sie mit einem macht.
Die Entscheidungen sind bei dir geblieben: was die Liste enthält, wann eine ID unbekannt ist und welcher Status das sagt.
Irgendetwas muss trotzdem Verbindungen annehmen und Bytes in Anfragen verwandeln: uvicorn, ein Serverprogramm, das Apps wie diese ausführt. Die Schnittstelle zwischen einem solchen Server und einer App heißt ASGI; mehr als den Namen brauchst du nicht.
Deine Aufgabe
Schreibe die Körper von list_books und get_book in reading_app.py:
list_booksgibt{"books": [...]}zurück: jedes Buch inSTATE["books"], in der Reihenfolge der IDs.get_book(book_id)gibt das Buch zurück oder löstHTTPException(status_code=404, detail="no such book")aus, wennSTATE["books"]keine solche ID hat.HTTPExceptionist schon importiert.
Noch gibt es keinen Weg, der App eine Anfrage zu senden; Lektion 2 fügt einen hinzu. Eine dekorierte Funktion ist immer noch eine Funktion, also rufe eine im Tab Shell auf:
python -c "import reading_app; print(reading_app.get_book(1))"
Es gibt {'id': 1, 'title': 'Kindred', 'author': 'Octavia E. Butler'} aus, das Dictionary so, wie Python es ausgibt, nicht als JSON. Klicke dann auf Check my work. Lektion 2 sendet der App echte Anfragen, ohne einen
Aufgabe
Schreibe die Körper von list_books und get_book in reading_app.py, unter die @app.get-Zeilen, die schon da sind.
list_booksgibt{"books": [...]}zurück, einealler Bücher in STATE["books"], in der Reihenfolge der IDs. Lektion 5 benutztlimit; ignoriere es vorerst.get_book(book_id)gibt das BuchSTATE["books"][book_id]zurück. WennSTATE["books"]kein Buch mit dieser ID hat, löst es stattdessenHTTPException(status_code=404, detail="no such book")aus.
Behalte jede @app-Zeile, jede def-Zeile, BookIn, STATE und reset_state so, wie sie mit dem Kapitel gekommen sind. Lies die Bücher aus STATE, das reset_state füllt; baue sie nicht innerhalb einer sample_state().
Im python -c "import reading_app; print(reading_app.get_book(1))" deine Funktion direkt auf.
Klicke dann auf Check my work. Es füllt STATE über reset_state mit Büchern, die du noch nicht gesehen hast, und sendet dann mit dem TestClient von GET /books, GET /books/ID für IDs, die es enthält, und für solche, die es nicht enthält, und GET /books/abc an die App in diesem Prozess: kein Server, kein detail von HTTPException tut. /books/abc bekommt die eigene 422 von FastAPI, egal was deine Funktion tut, also prüft diese Anfrage nur, dass die mitgelieferten Zeilen noch an ihrem Platz sind. Nichts aus deinem Terminal wird bewertet.