Chapter 12 · capstone
Capstone: The Reading List Service and Client
Capstone project: Finish the Server
Chapter 11 rebuilt the reading requests client from Chapter 10, and asks both to meet one document at once: SERVICE_CONTRACT.md.
The contract is frozen. Nothing in this chapter changes a route, a status, a header, or an exit code in it. Two programs now rely on the same words. When the server sends location: /books/3, the client turns it into a
Read the contract, find the gaps
Open SERVICE_CONTRACT.md in the Project tab and scroll to its last section, “Not built yet”. It lists three rows. Each row has a server half, which you restore in this lesson, and a client half, which the next two lessons build:
| Row | Server half (this lesson) | Client half |
|---|---|---|
| Old paths | every /v0/books and /v0/books/... gets a 308 to the same path without /v0, query kept | a followed 308 from URL line for each redirect the client followed (Lesson 3) |
| Revalidation | a matching If-None-Match turns a 200 read into a bodiless 304 | a cache file that show reuses while fresh and revalidates when stale (Lesson 2) |
| New books | the 201 for POST /books carries location: /books/{id} | add prints the new book’s absolute URL (Lesson 2) |
None of the server halves is new to you. The location line is Chapter 5’s, legacy_location and its 308 branch are Chapter 6’s, and etag_matches, not_modified, and the 304 rule are Chapter 8’s. They were taken out of an otherwise finished server so that the client halves have something to agree with.
The rest of reading_server.py is the reviewed server from the end of Chapter 9: read_body closing the connection after every refusal, login and sessions, bearer tokens, and etag and cache-control on every 200 read. Leave it as it is. Check my work proves those rows again, with books, users, and tokens you have not seen, because a change in one place can break a row far away.
Milestones
First, old paths. Write legacy_location(target): take the path with urlsplit(target).path, cut the first three characters, /v0, and add ? and the query when there is one. Work from the raw target, not the decoded segments, so the handle, before every other route: when the first two segments are v0 and books, the response is redirect(308, legacy_location(request["target"])). It comes first so that a write to an old path is redirected before the token is checked, and nothing is stored.
Then revalidation. Write etag_matches(values, etag): values is the list of if-none-match lines, and every line may hold several tags separated by commas. Strip each member, drop a leading W/, and return True for * or for a tag equal to etag. Write not_modified(response): a new response etag and cache-control copied, and the body b"". Then add the rule to handle, after routing and before the no-store and HEAD rules: when the response has an etag and the request sent if-none-match, and the two match, the response becomes not_modified(response). Only a 200 for a read carries an etag, so errors and writes are left alone, and * on a missing book still gets its 404.
Then the location. In create_book, add one line before the location set to /books/ and the new id. The client resolves this relative value in Lesson 2.
Two helper runs from earlier chapters still work on this server:
python pl_http.py moved
python pl_http.py conditional
In the moved run, your server’s log should show 308 for the two /v0 requests where the server you started this chapter with answered 404. In the conditional run, the log should read 200, 304, 304, 200: the first GET gets the tag, the next GET and the HEAD send it back and get 304, and the last request sends a stale tag and gets the whole list again.
Which client feature stops working if the server's 304 rule is missing?
Your tests
tests/test_server.py calls handle directly with make_request, as it has since Chapter 5. Run it after each milestone:
python -m pytest -q
Every test that came with the project should still pass. Lesson 4 adds tests for the three rows.
Your task
Write
legacy_location, and the 308 branch at the top ofhandle.Write
etag_matchesandnot_modified, and the 304 rule inhandle.Add
locationto the 201 increate_book.Run
python -m pytest -q, then the two helper runs.Press Check my work.
If a check fails, its message names the request and the rule. Ask Monty to read it beside the matching table of SERVICE_CONTRACT.md with you, without writing the fix.
The server now meets every server row of the contract. The client still downloads every book every time and prints a new book without its URL; the next lesson fixes both.
Task
Finish the server half of SERVICE_CONTRACT.md: the three server rows under “Not built yet”.
legacy_location(target): the target’s path without its first three characters,/v0, with?and the query added when there is one. At the top ofhandle, before every other route, a path whose first two segments arev0andbooksgetsredirect(308, legacy_location(request["target"])), for every. etag_matches(values, etag):Truewhen any member of anyif-none-matchline, stripped and with a leadingW/removed, equalsetag, or is*.not_modified(response): a 304 with only the 200’setagandcache-controland the bodyb"". Inhandle, after routing and before theno-storeandHEADrules, a response with anetagwhose request’sif-none-matchmatches it becomesnot_modified(response).In
create_book, the 201 carrieslocation: /books/{id}.
Keep every other row of the contract as the server already meets it. Run python -m pytest -q, python pl_http.py moved, and python pl_http.py conditional in the
Then press Check my work. It calls handle with books, users, and tokens you have not seen (ids 7, 41, and 1000, the user zoë), and checks: reads, HEAD, 404s, and 405 with allow; that content-length matches every body, on bytes your server’s handler writes in memory; the 201’s location, read against the request’s GET, HEAD, POST, PUT, and DELETE on /v0 paths with a percent-encoded query; login, the HttpOnly /me, and logout; 401 and 403 for writes; etag and cache-control with MAX_AGE set to 45; the 304 for one tag, a W/, and *, ending at the blank line; three requests on one connection, and a refusal that closes it; and make_server and serve with a stand-in server that counts connections. Nothing opens a