Chapter 11 · practice
What a Framework Does for You
Your Handler as Path Operations
Here is the part of your Chapter 9 handle that answers /books/{id}, from the reading_server.py in this chapter’s files, with the write branches left out:
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""
Read it as two kinds of line. A few lines decide something about the reading path_segments split and decoded the target before this branch, and the comparisons on segments routed the request here. book_id_from turns text into an int. json_response encodes the content-length. not_allowed refuses other allow list, and the last lines drop the body of a reply to HEAD. You wrote every one of them, so you know what a reply contains.
A framework takes the plumbing and leaves you the decisions. This chapter rewrites the reading list with reading_app.py. Your Chapter 9 server stays beside it, unchanged, for comparison. The app keeps the five routes of Chapter 5: GET and POST on /books, and GET, PUT and DELETE on /books/{id}. Login, tokens, redirects and
Reading the decorator
Open reading_app.py. Near the top, app = FastAPI() makes the application NotImplementedError, each with a line above it that starts with @:
@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}") registers the function below it with the app, for GET requests whose path matches /books/{book_id}. When such a request arrives, FastAPI calls get_book. Registering calls nothing, and the function itself stays an ordinary function: once you have written it, reading_app.get_book(1) works when you call it yourself, with no request anywhere.
A line that starts with @ above a def is a decorator. Python Fundamentals II teaches decorators, classes, and annotations in full. This course never asks you to write any of them: every @app line the chapter needs is already in the file, and you write function bodies only.
What does @app.get("/books/{book_id}") above def get_book(book_id: int): do?
Annotations FastAPI reads
The {book_id} in the path names a part of the book_id: int in the def line connects it to the request: dict was a note that Python ignores. FastAPI reads this one: it takes the path segment, turns it into an int before calling your function, and answers 422 itself when it cannot. So inside get_book, book_id is always an int. Your server answered 404 for /books/abc, because book_id_from returned None; this app answers 422.
The same file holds a model, a class that describes data:
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")
It describes the JSON object a POST or PUT body must be: an object whose title and author are list_books also has an annotated parameter, limit, which Lesson 5 uses; leave it alone for now.
A request asks for /books/abc. What answers?
What your function returns
A function registered this way is called a path operation, and it answers by returning a None, and FastAPI encodes it as JSON (None becomes null), sets content-type: application/json and a content-length that counts the bytes, and sends status 200. For GET /books, return {"books": [...]} with the books in id order, the shape your server sent.
For an unknown id, the function raises an
raise HTTPException(status_code=404, detail="no such book")
FastAPI sends a 404 whose JSON body is {"detail":"no such book"}. Your server said error where FastAPI says detail. FastAPI also writes JSON without spaces after : and ,, so the same books take fewer bytes than your server sent. Lesson 4 shows those bytes.
How does get_book answer 404 for an unknown id?
Where the books live
get_book(book_id) has no state parameter, because FastAPI passes a function only the parts of the request it asks for. The books live in a dictionary at the top of the STATE, with the shape of your server’s state: STATE["books"] maps ids to books, and STATE["next_id"] is the next id. reset_state(state) replaces its contents with the state you pass, or with the two sample books, and every test calls it first, so no test sees books another test added.
What disappeared
Your two functions will be about four lines each. Gone from your code:
path_segmentsand the comparisons onsegments: the paths in the@app.getlines route each request.book_id_from:book_id: intconverts the id.json_responseand its byte count: FastAPI encodes the dict and setscontent-length.the
not_allowedbranches: FastAPI answers 405 itself when no route for a path accepts the method.the HEAD rule: nothing in the app mentions
HEAD. Lesson 2 asks the app what it does with one.
The decisions stayed with you: what the list holds, when an id is unknown, and which status says so.
Something still has to accept connections and turn bytes into requests: uvicorn, a server program that runs apps like this one. The interface between such a server and an app is called ASGI; the name is all you need.
Your task
Write the bodies of list_books and get_book in reading_app.py:
list_booksreturns{"books": [...]}: every book inSTATE["books"], in id order.get_book(book_id)returns the book, or raisesHTTPException(status_code=404, detail="no such book")whenSTATE["books"]has no such id.HTTPExceptionis already imported.
There is no way yet to send the app a request; Lesson 2 adds one. A decorated function is still a function, so call one in the Shell tab:
python -c "import reading_app; print(reading_app.get_book(1))"
It prints {'id': 1, 'title': 'Kindred', 'author': 'Octavia E. Butler'}, the dict as Python prints it, not JSON. Then press Check my work. Lesson 2 sends the app real requests without opening a
Task
Write the bodies of list_books and get_book in reading_app.py, under the @app.get lines that are already there.
list_booksreturns{"books": [...]}, aof every book in STATE["books"], in id order. Lesson 5 useslimit; ignore it for now.get_book(book_id)returns the bookSTATE["books"][book_id]. WhenSTATE["books"]has no book with that id, it raisesHTTPException(status_code=404, detail="no such book")instead.
Keep every @app line, every def line, BookIn, STATE, and reset_state as they came with the chapter. Read the books from STATE, which reset_state fills; do not build them inside a sample_state().
In the python -c "import reading_app; print(reading_app.get_book(1))" calls your function directly.
Then press Check my work. It fills STATE through reset_state with books you have not seen, then sends GET /books, GET /books/ID for ids it holds and ids it does not, and GET /books/abc into the app in this process with TestClient, which Lesson 2 introduces: no server, no HTTPException’s detail does. /books/abc gets FastAPI’s own 422 whatever your function does, so that request checks only that the supplied lines are still in place. Nothing from your terminal is graded.