0%

Chapter 12 · capstone

Capstone: The Reading List Service and Client

Capstone project: Finish the Server

Chapter 11 rebuilt the reading in . This chapter goes back to the hand-built server and to the 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 and prints it; when the server answers 304, the client reuses a book it stored a minute ago. If either half drifts from the contract, the other half breaks, and its own code looks fine. Each half has passed its own tests before; what is new is holding both to one contract at the same time.

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:

RowServer half (this lesson)Client half
Old pathsevery on /v0/books and /v0/books/... gets a 308 to the same path without /v0, query kepta followed 308 from URL line for each redirect the client followed (Lesson 3)
Revalidationa matching If-None-Match turns a 200 read into a bodiless 304a cache file that show reuses while fresh and revalidates when stale (Lesson 2)
New booksthe 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: /1.1 with keep-alive, 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 stays ASCII. Then add the branch at the very top of 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 with status 304, the 200’s 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 : the 201’s headers get 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

  1. Write legacy_location, and the 308 branch at the top of handle.

  2. Write etag_matches and not_modified, and the 304 rule in handle.

  3. Add location to the 201 in create_book.

  4. Run python -m pytest -q, then the two helper runs.

  5. 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”.

  1. 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 of handle, before every other route, a path whose first two segments are v0 and books gets redirect(308, legacy_location(request["target"])), for every .

  2. etag_matches(values, etag): True when any member of any if-none-match line, stripped and with a leading W/ removed, equals etag, or is *. not_modified(response): a 304 with only the 200’s etag and cache-control and the body b"". In handle, after routing and before the no-store and HEAD rules, a response with an etag whose request’s if-none-match matches it becomes not_modified(response).

  3. In create_book, the 201 carries location: /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 and fetched again; PUT and DELETE; each refusal by its cause, in the contract’s order, with nothing stored; the 308 for 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 , two lines, 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 , and nothing from your terminal is graded.