0%

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 als en telt de bytes ervan in content-length. not_allowed weigert andere met een 405 en een allow-, en de laatste regels laten de body weg van een antwoord op 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 , een veelgebruikt webframework voor Python, in een nieuw bestand, 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, en blijven bij je eigen server.

De decorator lezen

Open reading_app.py. Bovenaan maakt app = FastAPI() het applicatieobject. Verderop staan vijf waarvan de body alleen 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 een naam, en book_id: int in de def-regel verbindt dat deel met de . In les 4.1 was de annotatie 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- dat de body van een POST of PUT moet zijn: een object waarvan title en author zijn met minstens één teken dat geen spatie is. FastAPI controleert elke body daartegen voordat je functie draait. Je gebruikt het in les 3. 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 terug te geven. Geef een dict, een lijst, een getal of 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 op die FastAPI omzet in een antwoord:

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_segments en de vergelijkingen op segments: de paden in de @app.get-regels routeren elk verzoek.

  • book_id_from: book_id: int zet het id om.

  • json_response en het tellen van de bytes: FastAPI codeert de dict en zet content-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:

  1. list_books geeft {"books": [...]} terug: elk boek in STATE["books"], op volgorde van id.

  2. get_book(book_id) geeft het boek terug, of werpt HTTPException(status_code=404, detail="no such book") op als STATE["books"] dat id niet heeft. HTTPException is 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 te openen.

Opdracht

Schrijf de body’s van list_books en get_book in reading_app.py, onder de @app.get-regels die er al staan.

  1. list_books geeft {"books": [...]} terug, een van elk boek in STATE["books"], op volgorde van id. Les 5 gebruikt limit; negeer dat voorlopig.

  2. get_book(book_id) geeft het boek STATE["books"][book_id] terug. Als STATE["books"] geen boek met dat id heeft, werpt het in plaats daarvan HTTPException(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 op uit sample_state().

In de roept 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 ’s TestClient, die les 2 introduceert: geen server, geen . Het roept ook je twee functies rechtstreeks aan. Een onbekend id moet een 404 krijgen waarvan de -body de reden als bevat, zoals de 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.