0%

Redirects · capstone

Capstone project: Project: Books That Moved

You have both halves of a redirect. Your server answers /v0/books paths with a 308 that keeps the id and the query, and your follow resolves a location, keeps or changes the by status, and stops at a hop limit. You ran them together through pl_http.py follow, but every check so far proved one half at a time, against stand-ins. And nothing you have written yet sends a write through the follower: follow takes a body, but no builds one. This project writes that function, send_json, and shows that an old client’s PUT reaches the book it meant, through your server’s redirect, in one hop.

What send_json does

send_json(method, url, data, *, make_connection, max_hops, timeout) is the client an old reading app would call to save a book:

  1. Encode data as in bytes: json.dumps(data, ensure_ascii=False).encode("utf-8"), as your server’s json_response does.

  2. Send it with follow, passing the body, a Content-Type: application/json header, and make_connection, max_hops and timeout exactly as send_json received them.

  3. Add "data" to the record follow returned: the body decoded as UTF-8 JSON when the reply’s content-type is application/json ( such as ; charset=utf-8 allowed) and the body is not empty, and None otherwise. get_json made the same decision in Chapter 3.

Every status is a record, as before. A 422 from the server comes back with its error in "data"; send_json raises only what follow raises, such as at the hop limit or TimeoutError when a server stays silent.

About eight lines do it, because the hard part is already in follow. Look at what follow does with this body when it meets a redirect: after a 308 the PUT and its body go to the new path unchanged; after a 303 the body and its content-type are dropped. Your server’s 308 and your follower’s rule for 308 have to agree, or an old client’s write is lost.

Milestones

First, write send_json. Then run the helper’s agree scenario:

python pl_http.py agree

It calls your send_json("PUT", "http://books.test/v0/books/2", ...) with a new title for book 2, against your own server on this machine. You should see two requests and then the data your server stored:

request 1  PUT /v0/books/2  ->  308 Permanent Redirect
request 2  PUT /books/2  ->  200 OK
...
data: {'id': 2, 'title': 'Café Society (2nd edition)', 'author': 'Ana Ruiz'}

Then the helper sends its own GET /books/2, and that reply carries the new title too: the write reached the current path, and only once.

Then the . Run the loop scenario:

python pl_http.py loop

Here your server runs a small handler written for this course that redirects in a circle: /a answers 307 to /b, and /b answers 307 to /a. The helper calls your follow("GET", "http://books.test/a", max_hops=3, ...). A correct follower sends four requests and raises ValueError; the transcript shows request 1 to request 4, all 307 Temporary Redirect, then raised ValueError with your message, and the scenario exits with status 0, because here the error is the right outcome. The helper gives the server five connections and opens the one your follower did not use itself, which it reports as pl_http opened 1 empty connection(s) so serve() could stop waiting. A follower with no limit keeps going until the server has stopped, and fails with ConnectionRefusedError instead.

Optional: test send_json yourself. fakes.py now has scripted_connections(responses, calls): a make_connection whose connections answer with the given responses in order, one per connection, and record each connection’s host, , timeout, requests and whether it was closed. Add scripted_connections to the from fakes import line of tests/test_wire.py, and a test such as:

def test_send_json_follows_a_308():
    calls = []
    moved = scripted_response(308, "Permanent Redirect", [["location", "/books/2"]], b"")
    stored = scripted_response(200, "OK", [["content-type", "application/json"]], b'{"id": 2, "title": "Dune"}')
    connections = scripted_connections([moved, stored], calls)
    record = wire.send_json("PUT", "http://books.test/v0/books/2", {"title": "Dune"}, make_connection=connections)
    assert record["data"] == {"id": 2, "title": "Dune"}
    assert calls[1]["requests"][0]["method"] == "PUT"
    assert calls[1]["closed"]

Run your tests from the project folder:

python -m pytest -q

They are yours; Check my work does not run them.

What is proven, and how

Check my work proves the agreement without a . It connects your send_json to your own reading_server.handle in one process: the connection it hands to your follower builds a request from what your code sends, passes it through your read_body and handle, as http_adapter would, and turns the response dict back into a reply. So the 308 your follower reads is the one your server wrote, and the PUT it repeats changes the state your server keeps. The agree and loop runs show the same thing over a real connection on this machine. Those runs are for you to watch; they are not graded.

Then press Check my work.

Every request in this course so far has stood alone: the server forgot each client the moment it answered, and no client could prove who it was. Chapter 7 gives the reading list a memory of who is asking, with a session kept in a and a token that decides who may write.

Task

Write send_json(method, url, data, *, make_connection, max_hops, timeout) in wire.py, so a write goes through your follower and reaches the current path.

  • Encode data as JSON in bytes.

  • Send it with follow(method, url, ...), with that body, a Content-Type: application/json header, and the make_connection, max_hops and timeout that send_json was given.

  • Return the record follow returned, with "data" added: the body decoded as UTF-8 JSON when the reply’s content-type is application/json (with or without ) and the body is not empty, otherwise None.

  • Any status is a record. Let at the hop limit, and every other exception from follow, leave send_json.

Your server’s redirects from Lesson 3 and your follow from Lesson 4 must still work: this check runs them together. Run python pl_http.py agree and python pl_http.py loop in the to watch them on a real connection.

Then press Check my work. It hands send_json a make_connection whose connections pass each request, as a request , through your own read_body and reading_server.handle, with a state of books you have not seen, and turn your server’s response dict back into a reply; no is opened. It checks that a PUT to an old path with a query changes the book after exactly one 308 and returns 200 with the stored book in "data"; that a POST to /v0/books creates a book with 201 after one 308; that a 422 comes back as a record with the error in "data"; that a reply with no body, or with a body that is not JSON, gives "data" None; that a POST answered with 303 continues as a GET with no body; that a redirect raises ValueError after max_hops + 1 requests; and that the timeout reaches every connection. Your own tests are not run. Nothing from your terminal is graded.