Hoofdstuk 11 · oefening
Wat een framework voor je doet
Je handler als padoperaties
Hier is het deel van je handle uit hoofdstuk 9 dat /books/{id} beantwoordt, uit de reading_server.py in de bestanden van dit hoofdstuk, zonder de takken die schrijven:
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""
Lees het als twee soorten regels. Een paar regels beslissen iets over de leeslijst: een id wijst alleen een boek aan als de state het bevat, een onbekend boek is een 404, en een gevonden boek is het antwoord. De rest is loodgieterswerk. path_segments splitste en decodeerde het doel vóór deze tak, en de vergelijkingen op segments stuurden het verzoek hierheen. book_id_from maakt van tekst een int. json_response codeert de content-length. not_allowed weigert andere allow-HEAD. Je hebt ze allemaal zelf geschreven, dus je weet wat een antwoord bevat.
Een framework neemt het loodgieterswerk over en laat de beslissingen aan jou. Dit hoofdstuk herschrijft de leeslijst met reading_app.py. Je server uit hoofdstuk 9 blijft er ongewijzigd naast staan, om te vergelijken. De app houdt de vijf routes van hoofdstuk 5: GET en POST op /books, en GET, PUT en DELETE op /books/{id}. Inloggen, tokens,
De decorator lezen
Open reading_app.py. Bovenaan maakt app = FastAPI() het applicatieobject. Verderop staan vijf NotImplementedError opwerpt, elk met een regel erboven die met @ begint:
@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}") registreert de functie eronder bij de app, voor GET-verzoeken waarvan het pad overeenkomt met /books/{book_id}. Als zo’n verzoek binnenkomt, roept FastAPI get_book aan. Registreren roept niets aan, en de functie zelf blijft een gewone functie: zodra je hem hebt geschreven, werkt reading_app.get_book(1) als je hem zelf aanroept, zonder dat er ergens een verzoek is.
Een regel die met @ begint boven een def is een decorator. Python Fundamentals II behandelt decorators, klassen en annotaties volledig. Deze cursus vraagt je nooit er zelf een te schrijven: elke @app-regel die het hoofdstuk nodig heeft, staat al in het bestand, en jij schrijft alleen de body’s van functies.
Wat doet @app.get("/books/{book_id}") boven def get_book(book_id: int):?
Annotaties die FastAPI leest
De {book_id} in het pad geeft een deel van de book_id: int in de def-regel verbindt dat deel met de request: dict een notitie die Python negeert. Deze leest FastAPI wel: het neemt het padsegment, maakt er een int van voordat het je functie aanroept, en antwoordt zelf met 422 als dat niet lukt. Binnen get_book is book_id dus altijd een int. Je server antwoordde 404 op /books/abc, omdat book_id_from None teruggaf; deze app antwoordt 422.
Hetzelfde bestand bevat een model, een klasse die gegevens beschrijft:
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")
Het beschrijft het JSON-title en author list_books heeft ook een geannoteerde parameter, limit, die les 5 gebruikt; laat die voorlopig met
Een verzoek vraagt om /books/abc. Wie antwoordt?
Wat je functie teruggeeft
Een functie die zo is geregistreerd, heet een padoperatie (path operation), en ze antwoordt door een None terug, en FastAPI codeert die als JSON (None wordt null), zet content-type: application/json en een content-length die de bytes telt, en stuurt status 200. Geef voor GET /books {"books": [...]} terug met de boeken op volgorde van id, de vorm die je server stuurde.
Voor een onbekend id werpt de functie een
raise HTTPException(status_code=404, detail="no such book")
FastAPI stuurt een 404 waarvan de JSON-body {"detail":"no such book"} is. Waar je server error zei, zegt FastAPI detail. FastAPI schrijft JSON ook zonder spaties na : en ,, dus dezelfde boeken nemen minder bytes in dan je server stuurde. Les 4 laat die bytes zien.
Hoe antwoordt get_book met 404 voor een onbekend id?
Waar de boeken staan
get_book(book_id) heeft geen parameter state, omdat FastAPI een functie alleen de delen van het verzoek geeft waar ze om vraagt. De boeken staan in een dictionary bovenaan de STATE, met de vorm van de state van je server: STATE["books"] koppelt id’s aan boeken, en STATE["next_id"] is het volgende id. reset_state(state) vervangt de inhoud door de state die je meegeeft, of door de twee voorbeeldboeken, en elke test roept het als eerste aan, zodat geen enkele test boeken ziet die een andere test heeft toegevoegd.
Wat er verdween
Je twee functies worden elk ongeveer vier regels. Uit je code verdwenen:
path_segmentsen de vergelijkingen opsegments: de paden in de@app.get-regels routeren elk verzoek.book_id_from:book_id: intzet het id om.json_responseen het tellen van de bytes: FastAPI codeert de dict en zetcontent-length.de takken met
not_allowed: FastAPI antwoordt zelf met 405 als geen enkele route voor een pad de methode accepteert.de HEAD-regel: niets in de app noemt
HEAD. Les 2 vraagt de app wat hij met zo’n verzoek doet.
De beslissingen bleven bij jou: wat de lijst bevat, wanneer een id onbekend is, en welke status dat zegt.
Er moet nog steeds iets verbindingen accepteren en bytes omzetten in verzoeken: uvicorn, een serverprogramma dat apps zoals deze draait. De interface tussen zo’n server en een app heet ASGI; de naam is alles wat je nodig hebt.
Je opdracht
Schrijf de body’s van list_books en get_book in reading_app.py:
list_booksgeeft{"books": [...]}terug: elk boek inSTATE["books"], op volgorde van id.get_book(book_id)geeft het boek terug, of werptHTTPException(status_code=404, detail="no such book")op alsSTATE["books"]dat id niet heeft.HTTPExceptionis al geïmporteerd.
Er is nog geen manier om de app een verzoek te sturen; les 2 voegt er een toe. Een gedecoreerde functie is nog steeds een functie, dus roep er een aan in het tabblad Shell:
python -c "import reading_app; print(reading_app.get_book(1))"
Het drukt {'id': 1, 'title': 'Kindred', 'author': 'Octavia E. Butler'} af, de dict zoals Python die afdrukt, geen JSON. Druk daarna op Check my work. Les 2 stuurt de app echte verzoeken zonder een
Opdracht
Schrijf de body’s van list_books en get_book in reading_app.py, onder de @app.get-regels die er al staan.
list_booksgeeft{"books": [...]}terug, eenvan elk boek in STATE["books"], op volgorde van id. Les 5 gebruiktlimit; negeer dat voorlopig.get_book(book_id)geeft het boekSTATE["books"][book_id]terug. AlsSTATE["books"]geen boek met dat id heeft, werpt het in plaats daarvanHTTPException(status_code=404, detail="no such book")op.
Laat elke @app-regel, elke def-regel, BookIn, STATE en reset_state zoals ze met het hoofdstuk meekwamen. Lees de boeken uit STATE, dat reset_state vult; bouw ze niet binnen een sample_state().
In de python -c "import reading_app; print(reading_app.get_book(1))" je functie rechtstreeks aan.
Druk daarna op Check my work. Het vult STATE via reset_state met boeken die je niet hebt gezien, en stuurt daarna GET /books, GET /books/ID voor id’s die erin staan en id’s die er niet in staan, en GET /books/abc de app in, in hetzelfde proces, met TestClient, die les 2 introduceert: geen server, geen detail van HTTPException dat doet. /books/abc krijgt FastAPI’s eigen 422, wat je functie ook doet, dus dat verzoek controleert alleen of de meegeleverde regels er nog staan. Niets uit je terminal wordt beoordeeld.