0%

The Standard Client and Status Classes · capstone

Capstone project: Project: A Standard-Library Client

get_json, classify, and report work, and you have watched them against practice-api one scenario at a time. A run against a live service shows that one case worked once. Tests on a stand-in connection show more: every class, the error bodies, a timeout and the closing, each time you run them, with no server needed. This project finishes the client and proves it with tests of your own.

What the client promises

When this project is done, wire.py keeps these promises, and your tests check them:

  • get_json returns the full record for a response of any class: "status", "reason", "headers", "content_type", "retry_after", "body", "class", and "data".

  • The body of every 4xx and 5xx is kept, and decoded into "data" when it is labeled .

  • When no response arrives, TimeoutError leaves get_json, and the connection is closed.

  • report turns a timeout into a line that names no status, and lets every other failure through.

If any of these does not hold yet, fix it first. If Lessons 1 to 5 all passed, your code may need no change.

The two fakes

Open fakes.py and read the two after fake_connect. scripted_response(status, reason, header_pairs, body) builds a response: header_pairs is a of [name, value] lists in the order a server would send them, and getheader finds a name in any casing, as the real one does. fake_make_connection(response, calls) returns a stand-in for http.client.HTTPConnection. Pass it as make_connection=, and every step your code takes lands in calls, in order:

{"call": "connect", "host": "practice-api", "port": 8080, "timeout": 2.0}
{"call": "request", "method": "GET", "url": "/v1/items/1", "body": None, "headers": {"X-Practice-Name": "http-with-python"}}
{"call": "close"}

With None as the response, getresponse() raises TimeoutError, as a silent server makes the real one do. Everything a test needs is there: a scripted status, headers in any casing, a body, and a record of whether the connection was closed.

One test, worked through

Here is the shape of a test for a success, with the first two assertions written out:

def test_get_json_decodes_a_success():
    calls = []
    response = scripted_response(200, "OK", [["Content-Type", "application/json"]], b'{"id": 1}')
    record = wire.get_json("/v1/items/1", make_connection=fake_make_connection(response, calls))
    assert record["class"] == "success"
    assert record["data"] == {"id": 1}

Arrange a response and an empty calls list, act by calling wire.get_json, then assert on the record and on calls. Add fake_make_connection and scripted_response to the from fakes import line at the top of tests/test_wire.py.

For a test that expects an , the file already has the pattern: test_a_close_inside_the_body_is_an_error calls the function inside try, returns from the except clause for the exception it expects, and raises AssertionError after the try if the call returned instead. calls[-1] is the last step recorded, which is where a close belongs.

Milestones

  1. A success test, like the one above, that also checks the connect entry in calls and that the last entry is the close.

  2. One test for a 4xx and one for a 5xx, each with a JSON error body, asserting the class and the decoded "data".

  3. A test with fake_make_connection(None, calls) that expects TimeoutError from get_json and then checks that calls ends with the close.

  4. A report test: a scripted 504 gives a line that starts with 504, and a silent server gives a line that says timed out and holds no 504.

  5. Anything else worth proving, such as Retry-After in odd casing, or classify on a code no list of yours names.

Write each test as a plain def test_...(): function with no , as the file’s other tests are. Keep import wire, and call wire.get_json(...), wire.report(...), and wire.classify(...) inside each test; do not add a from wire import line or store those functions in names at the top of the file. Check my work swaps in other versions of these functions through the to see whether your tests notice, and a name imported with from would keep the original.

Run it

Run the tests from the project folder:

python -m pytest -q

Every test should pass, the ones from Chapters 1 and 2 included, and none of them needs practice-api. If you like, run report over the routes in README.md as well; this run is for you and is never graded:

python -c "
import wire
for target in ['/v1/items?page=1&page_size=2', '/v1/scenarios/rate-limit?attempt=3', '/v1/items/99', '/v1/scenarios/server-error', '/v1/scenarios/timeout?delay_ms=750']:
    print(target, wire.report(target, timeout=0.5))
"

The first four lines name a status and its class: 200 success, then 429 client error, 404 client error, and 500 server error. The last one says your client timed out, because half a second is shorter than the scenario’s 750 milliseconds.

Then press Check my work. Your client now reads any reply practice-api can send and says what happened when none comes. In Chapter 4 the connection turns around: you write the server, and wire.py becomes the tool that shows you exactly what your server sent.

Task

Finish the standard-library client and prove it with your own tests.

In wire.py, make sure that get_json returns the full record ("status", "reason", "headers", "content_type", "retry_after", "body", "class", "data") for a response of any class, keeps and decodes error bodies, and lets TimeoutError through with the connection closed; that classify names the class of any code from 100 to 599; and that report turns only a timeout into a line. If Lessons 1 to 5 passed, this may already be true.

In tests/test_wire.py, write at least five tests that call wire.get_json, wire.report, or wire.classify, using fakes.scripted_response and fakes.fake_make_connection. Cover a success, a 4xx and a 5xx with JSON error bodies, a timeout (fake_make_connection(None, calls)) followed by a check that the connection was closed, and report on a 504 and on a timeout. Write plain def test_...(): with no . Keep import wire and call the functions through it inside each test; add no from wire import line and do not store them in names at the top of the file. Keep the tests the file already has.

Run python -m pytest -q in the .

Then press Check my work. It calls get_json, classify, and report with a stand-in connection and with statuses, headers and bodies you have not seen. Then it runs your tests, first against your own wire.py and then against four broken clients: one that calls every 4xx a success, one that throws away the body of every 4xx and 5xx, one that reports its own timeout as a 504, and one that leaves the connection open when no response arrives. Your tests must all pass against the first and at least one must fail against each of the others. A test that reaches for the network fails instead of reaching it. Nothing from your terminal is graded.