Capstone: The Reading List Service and Client · capstone
Capstone project: Run Them Together
Each half has been proven alone, and the two have never talked to each other. This lesson runs eleven commands of your client against your server, starting from reading_server.sample_state() with the now values a test would pass, and every step has to match the contract:
| Step | Command | now | What should happen | Exit |
|---|---|---|---|---|
| 1 | list | 0 | GET /books, two books | 0 |
| 2 | add, writer’s token | 0 | POST /books, 201, the new | 0 |
| 3 | show 3 | 0 | GET /books/3, fresh until 60 | 0 |
| 4 | show 3 | 30 | nothing sent | 0 |
| 5 | show 3 | 60 | If-None-Match, 304, fresh until 120 | 0 |
| 6 | update 3 via /v0, reader’s token | 60 | 308, then PUT /books/3: 403 | 2 |
| 7 | update 3 via /v0, writer’s token | 60 | 308, then 200 with the new title | 0 |
| 8 | show 3 | 90 | nothing sent, the old title | 0 |
| 9 | show 3 | 120 | If-None-Match, 200, the new title | 0 |
| 10 | delete 3 via /v0 | 120 | 308, then 204 | 0 |
| 11 | show 3 | 200 | If-None-Match, 404 | 2 |
Every row of “Not built yet” is in there. Steps 6, 7, and 10 cross the old path, step 5 revalidates, step 2 prints a location, and the server’s statuses decide each exit code.
A redirect you can see
In steps 6, 7, and 10 requests follows the 308 for you, as Chapter 10 showed: it sends the same response.history is a response.url is where requests ended up. For step 7, history holds one 308 whose url is the /v0 URL.
The contract asks the client to say so: one line on standard error per redirect followed, followed STATUS from URL, before any other line. Standard output stays for results, so a script that reads the book list never sees the line. Write report_redirects(response), a two-line response.history that prints each entry’s status_code and url with file=sys.stderr. Then call it in run for every command that got a response: right after call for list, add, update, and delete, and for show when cached_show returned a response, before the error check in both places.
update 3 through /v0 with the writer's token succeeds. How many followed lines appear, and where?
Watch it run
pl_http.py starts your server on a main, the now values above, and a cache file in a temporary folder. URLs read http://books.test, because it points requests at your server without a name lookup.
python pl_http.py capstone
For each command it prints the requests your server received, then the
--- requests and replies ---
--- your reading_client.py show 3 --base-url http://books.test (now 30) ---
exit status: 0
standard output: ['3: Piranesi by Susanna Clarke']
standard error: []
--- requests and replies ---
request 1 GET /books/3 (If-None-Match: "6a584f0c2da517c8750cdd3157f9d6f9cc07bbd19955c92a953fcc8cb494f8a9") -> 304 Not Modified (connection 1)
The first block has no request at all: the entry stored at step 3 was fresh. Step 6 crosses the old path with a token that may only read:
request 1 PUT /v0/books/3 -> 308 Permanent Redirect (connection 1)
request 2 PUT /books/3 -> 403 Forbidden (connection 1)
--- your reading_client.py update 3 --base-url http://books.test/v0 --token tok-ben-reads-K9pz --title "Piranesi (paperback)" --author "Susanna Clarke" (now 60) ---
exit status: 2
standard output: []
standard error: ['followed 308 from http://books.test/v0/books/3', '403 this token may read the list but not change it']
Two requests reached your server for one command. The second got a 403, not a 401, so requests sent the token again after the redirect: it keeps authorization when the new URL is on the same host. The standard-error list starts with your followed line and ends with the one error line. Every request says connection 1: requests kept one connection open across all eleven commands, as Chapter 9 taught your server to allow. At the end, pl_http opens two empty connections so that serve() reaches its count of three and returns.
Fresh is not the same as current
Step 8 is the one that looks wrong:
--- your reading_client.py show 3 --base-url http://books.test (now 90) ---
exit status: 0
standard output: ['3: Piranesi by Susanna Clarke']
At step 7 the title became Piranesi (paperback), and your server knows it. Your client printed the old title anyway, and sent nothing. It is right to. At step 5 the server’s 304 said max-age=60: you may reuse this copy for 60 seconds without asking. That promise ran until 120, and the change at step 7 cannot reach a copy the client never asks about. Step 9, at 120, is when the promise ends: the client sends the old tag, the tag no longer matches, and the 200 brings the new title. max-age trades freshness for requests saved, and the server chooses the trade.
Step 8 prints the old title, though step 7 changed it. Is the client wrong?
What is proven, and how
Check my work does not read that run. It repeats every check of Lessons 1 and 2, then runs eleven commands in memory: a real requests.Session whose only adapter hands each request’s bytes to your server’s handler class in the same process, so requests follows the 308s and reads the 304 as it would on a network. The books, titles, tokens, and base URL are new, MAX_AGE is 45, and every exit status, output line, followed line, and request count must match, with TIMEOUT on every hop.
Your task
Write
report_redirects.Call it in
runfor every command, before the error check.Run
python -m pytest -qandpython pl_http.py capstone.Press Check my work.
The two halves now agree on every command of the contract. The next lesson writes the tests that would notice if one of them stopped.
Task
Report every redirect your client followed, and run both halves together.
report_redirects(response): for each entry ofresponse.history, in order, printfollowed STATUS from URLon standard error, with the entry’sstatus_codeandurl. Nothing for an empty history.In
run, call it for every command that got a response (forshow, whencached_showreturned one), before the error check, so thefollowedlines come before any other standard-error line.
Keep every server and client row from Lessons 1 and 2. Run python -m pytest -q and python pl_http.py capstone in the
Then press Check my work. It repeats every check of Lessons 1 and 2, then checks report_redirects on a reply that followed a 308 and a 301, and the first standard-error line of list, show, add, update, delete, and a refused update, each through a /v0 base http://shelf.test:8123, MAX_AGE set to 45, and now 0, 0, 0, 30, 60, 60, 60, 90, 120, 120, 200. Every exit status, standard-output line, followed line, error line, request count, If-None-Match, and the timeout on every hop must match; then main with no session must make its own with make_session. While your client runs, reading_client.requests cannot send anything. It does not read your terminal or the pl_http.py run.