Capstone: A Live Scoreboard · capstone
Capstone project: Finish the Client CLI
Until now you have stood on the server’s side of the scoreboard, deciding what to send, when to refuse and how to close. This lesson stands you on the other side of your own protocol. Everything the server decides reaches a client as a message, a
scoreboard_client.py is half written. client_parser() declares the two commands, watch and score, and their options. The supplied main(argv=None, *, connect=connect) parses them, refuses a bad command line with parser.error (exit status 2: --points outside 1 to 10, a blank --team, a --wait out of score and returns 1 with one line on standard error if it cannot, and then returns whatever watch(...) or score(...) returns. format_snapshot(snapshot) turns a snapshot into one line, teams in the order the message
What does this print?
snapshot = {"version": 3, "scores": {"Owls": 4, "Foxes": 0}}
parts = []
for team in snapshot["scores"]:
parts.append(f"{team} {snapshot['scores'][team]}")
line = f"version {snapshot['version']}: " + ", ".join(parts)
print(line)
Never go backwards
watch(url, max_messages, wait, *, connect=connect) connects, prints one line per snapshot with format_snapshot, and stops after max_messages lines. It applies the rule from Chapter 8’s lesson on reconnecting: a client never shows a board older than one it has already shown. Keep the version of the last snapshot printed, start below any real version (-1 works), and skip a snapshot whose version is not greater. Only printed lines count toward --max-messages.
The server already sends versions in order, but the client does not rely on the server alone: it keeps the rule even if a server, a proxy or a later version of your own code does not. Not every message is a snapshot. A valid error envelope is meant for a scorekeeper, so watch skips it. A score envelope breaks the protocol, because only the scorekeeper sends scores, and so does anything parse_message refuses.
watch has printed version 7 and then receives a snapshot with version 6. What does it do?
How it ended decides the exit status
Every way the conversation can end has one exit status in the contract. For watch:
| How the conversation ended | Exit status |
|---|---|
max_messages lines printed | 0 |
| a close with 1000 or 1001 | 0 |
| a close with any other code | 1 |
the connection ended without a close exc.rcvd is None, the 1006 of Chapter 6) | 1 |
the InvalidStatus, with the status in exc.response.status_code) | 1 |
no snapshot within wait seconds, or no handshake answer in time (TimeoutError) | 1 |
nothing listens there (ConnectionRefusedError, or another OSError) | 1 |
| a message that breaks the protocol: close with 1008 first | 1 |
a valid error envelope | skipped |
Every exit with 1 writes exactly one line to standard error that says why, with print(..., file=sys.stderr); standard output holds only snapshot lines. A refused handshake names its status, as in HTTP 503.
Why 1008 for a bad message? RFC 6455 section 7.4.1 makes 1008, policy violation, the generic code when no more specific one fits. 1003 is for a kind of data the endpoint cannot accept, such as a binary frame, and invalid no such path close is.
Two facts decide the order of your except clauses. TimeoutError is a kind of OSError, so its clause must come before an except OSError: that would otherwise catch it with the wrong message. And InvalidStatus is not an OSError at all: it is a WebSocketException, so it needs a clause of its own. Pass open_timeout, close_timeout and proxy=None to connect, as Chapter 6 did, and pass the wait to every recv, so no silent server can keep the client for ever.
Why must except TimeoutError: come before except OSError: in watch?
The token goes in a header
score(url, token, team, points, *, connect=connect) sends one score and reports the answer. The token travels in the handshake, as an extra header:
headers = {"Authorization": f"Bearer {token}"}
with connect(url, additional_headers=headers, open_timeout=WAIT, close_timeout=WAIT, proxy=None) as connection:
connection.send(text)
On a local computer, a server’s process_request hook read exactly one Bearer and the token, from a handshake sent that way. Chapter 9 also accepted ?token= in the
Build the score envelope with scoreboard_messages.encode_message, send it once, and read one answer with recv(timeout=WAIT). A snapshot answer is printed with format_snapshot, exit 0. An error envelope, a refused handshake, a close before the answer, a timeout or no connection is one line on standard error, exit 1. The line for a refused handshake names the status, such as HTTP 401, and never the token: the token appears on no stream.
watch does not reconnect when its connection drops. Reconnecting with backoff, from Chapter 8, stays something you can recognize rather than something this client does: it ends, and its exit status says how.
Why does score send the token in an Authorization header and not as ?token= in the URL?
Your task
Write watch and score. Most of each is except clauses. Then run seven commands of your client against a scoreboard written for this course, so that a mistake you see is the client’s and not your server’s:
python pl_ws.py client-main
A finished version, run on the site, printed, after the list of commands:
1 watch | version 0: Owls 0, Foxes 0, Herons 0
1 watch | version 1: Owls 3, Foxes 0, Herons 0
1 watch | version 2: Owls 3, Foxes 2, Herons 0
1 watch exited with 0
2 score | version 1: Owls 3, Foxes 0, Herons 0
2 score exited with 0
3 score | version 2: Owls 3, Foxes 2, Herons 0
3 score exited with 0
4 wrong token ! the scoreboard refused the connection: HTTP 401
4 wrong token exited with 1
5 unknown team ! the scoreboard refused the score: unknown team
5 unknown team exited with 1
Lines with | went to standard output and lines with ! to standard error. Command 6, --points 11, is refused by the supplied main with its usage and exit status 2, and command 7 watches a
python -m pytest -q
echo $?
The six assert False tests still fail, so echo $? prints 1: you have not written a test of your own for either program yet. That is Lesson 4.
Task
In scoreboard_client.py, write two main already refuses a bad client_parser, read_token and format_snapshot as they are.
watch(url, max_messages, wait, *, connect=connect): connect withconnect(url, open_timeout=WAIT, close_timeout=WAIT, proxy=None)in awithblock; callconnection.recv(timeout=wait)for each message; print a snapshot withformat_snapshotonly when its version is greater than the last one printed, and return 0 aftermax_messageslines. Skip a validerrorenvelope. For anythingscoreboard_messages.parse_messagerefuses, and for ascoreenvelope, callconnection.close(1008, ...), then return 1. Return 0 for a close with 1000 or 1001, and 1 for any other close, forexc.rcvdofNone, forInvalidStatus,TimeoutError, and any otherOSError.score(url, token, team, points, *, connect=connect): send one score envelope, built withscoreboard_messages.encode_message, on a connection opened withadditional_headers={"Authorization": f"Bearer {token}"},open_timeout=WAIT,close_timeout=WAITandproxy=None; read one answer withrecv(timeout=WAIT). Print a snapshot answer withformat_snapshotand return 0. Return 1 for anerrorenvelope,InvalidStatus,ConnectionClosed,TimeoutErrorand any otherOSError.
Every return of 1 writes exactly one line to standard error, and a refused exc.response.status_code. Standard output holds only snapshot lines. Neither function ever prints the token or puts it in the
Run python pl_ws.py client-main and python -m pytest -q. Then press Check my work. It runs Lessons 1 and 2’s checks again, then calls the supplied main with a stand-in connect whose connection hands out scripted messages, records each recv timeout and records send and close; refused handshakes are the library’s own InvalidStatus. watch --max-messages 3 --wait 1.5 given versions 5, 4, 5, 6, 7, 8 must print versions 5, 6 and 7 and return 0, with proxy=None and the wait on every recv. Each ending in the table of this lesson must give its exit status, with exactly one line on standard error for each 1 and a close with 1008 for a message that breaks the protocol. score must connect to the URL given, send exactly one Authorization Bearer and a token you have not seen, send one score whose Headers, its name in any case, and the timeout may be given by position.