Eindproject: een live scorebord · eindproject
Eindproject: Maak de client-CLI af
Tot nu toe stond je aan de kant van de server van het scorebord, en besliste je wat je stuurde, wanneer je weigerde en hoe je sloot. Deze les zet je aan de andere kant van je eigen protocol. Alles wat de server beslist, bereikt een client als een bericht, een
scoreboard_client.py is half geschreven. client_parser() declareert de twee opdrachten, watch en score, en hun opties. De meegeleverde main(argv=None, *, connect=connect) parset ze, weigert een slechte opdrachtregel met parser.error (exitstatus 2: --points buiten 1 tot en met 10, een lege --team, een --wait buiten bereik), leest bij score het tokenbestand en geeft 1 terug met één regel op standard error als dat niet lukt, en geeft daarna terug wat watch(...) of score(...) teruggeeft. format_snapshot(snapshot) maakt van een snapshot één regel, met de teams in de volgorde waarin het bericht ze opsomt.
Wat drukt dit af?
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)
Nooit achteruit
watch(url, max_messages, wait, *, connect=connect) maakt verbinding, drukt met format_snapshot één regel per snapshot af, en stopt na max_messages regels. Hij past de regel toe uit de les van hoofdstuk 8 over opnieuw verbinden: een client toont nooit een bord dat ouder is dan een bord dat hij al heeft getoond. Onthoud de versie van de laatst afgedrukte snapshot, begin onder elke echte versie (-1 werkt), en sla een snapshot over waarvan de versie niet groter is. Alleen afgedrukte regels tellen mee voor --max-messages.
De server stuurt versies al op volgorde, maar de client vertrouwt niet alleen op de server: hij houdt de regel aan, ook als een server, een proxy of een latere versie van je eigen code dat niet doet. Niet elk bericht is een snapshot. Een geldige error-envelop is bedoeld voor een scorebijhouder, dus watch slaat die over. Een score-envelop breekt het protocol, omdat alleen de scorebijhouder scores stuurt, en dat geldt ook voor alles wat parse_message weigert.
watch heeft versie 7 afgedrukt en ontvangt daarna een snapshot met versie 6. Wat doet hij?
Hoe het eindigde, bepaalt de exitstatus
Elke manier waarop het gesprek kan eindigen, heeft in het contract één exitstatus. Voor watch:
| Hoe het gesprek eindigde | Exitstatus |
|---|---|
max_messages regels afgedrukt | 0 |
| een close met 1000 of 1001 | 0 |
| een close met een andere code | 1 |
de verbinding eindigde zonder close-exc.rcvd is None, de 1006 uit hoofdstuk 6) | 1 |
de InvalidStatus, met de status in exc.response.status_code) | 1 |
geen snapshot binnen wait seconden, of geen antwoord op de handshake op tijd (TimeoutError) | 1 |
er luistert niets op die plek (ConnectionRefusedError, of een andere OSError) | 1 |
| een bericht dat het protocol breekt: eerst sluiten met 1008 | 1 |
een geldige error-envelop | overgeslagen |
Elke exit met 1 schrijft precies één regel naar standard error die zegt waarom, met print(..., file=sys.stderr); standard output bevat alleen snapshotregels. Een geweigerde handshake noemt zijn status, zoals in HTTP 503.
Waarom 1008 voor een slecht bericht? RFC 6455 sectie 7.4.1 maakt 1008, policy violation, de algemene code als er geen specifiekere past. 1003 is voor een soort data die het eindpunt niet kan accepteren, zoals een binair frame, en ongeldige no such path van de server.
Twee feiten bepalen de volgorde van je except-clausules. TimeoutError is een soort OSError, dus zijn clausule moet vóór een except OSError: komen die hem anders met de verkeerde melding zou vangen. En InvalidStatus is helemaal geen OSError: het is een WebSocketException, dus het heeft een eigen clausule nodig. Geef open_timeout, close_timeout en proxy=None mee aan connect, zoals in hoofdstuk 6, en geef het wachten mee aan elke recv, zodat geen stille server de client eeuwig kan vasthouden.
Waarom moet except TimeoutError: in watch vóór except OSError: komen?
Het token gaat in een header
score(url, token, team, points, *, connect=connect) stuurt één score en meldt het antwoord. Het token reist mee in de handshake, als 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)
Op een lokale computer las de process_request-hook van een server precies één Bearer en het token, uit een handshake die zo was verstuurd. Hoofdstuk 9 accepteerde ook ?token= in de
Bouw de score-envelop met scoreboard_messages.encode_message, stuur hem één keer, en lees één antwoord met recv(timeout=WAIT). Een snapshot als antwoord wordt afgedrukt met format_snapshot, exit 0. Een error-envelop, een geweigerde handshake, een close vóór het antwoord, een time-out of geen verbinding is één regel op standard error, exit 1. De regel voor een geweigerde handshake noemt de status, zoals HTTP 401, en nooit het token: het token verschijnt op geen enkele stream.
watch maakt niet opnieuw verbinding als zijn verbinding wegvalt. Opnieuw verbinden met backoff, uit hoofdstuk 8, blijft iets wat je kunt herkennen in plaats van iets wat deze client doet: hij eindigt, en zijn exitstatus zegt hoe.
Waarom stuurt score het token in een Authorization-header en niet als ?token= in de URL?
Je opdracht
Schrijf watch en score. Het grootste deel van elk is except-clausules. Draai daarna zeven opdrachten van je client tegen een scorebord dat voor deze cursus is geschreven, zodat een fout die je ziet van de client is en niet van je server:
python pl_ws.py client-main
Een afgemaakte versie, op de site uitgevoerd, drukte na de
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
Regels met | gingen naar standard output en regels met ! naar standard error. Opdracht 6, --points 11, wordt door de meegeleverde main geweigerd met zijn gebruiksaanwijzing en exitstatus 2, en opdracht 7 kijkt naar een
python -m pytest -q
echo $?
De zes tests met assert False falen nog steeds, dus echo $? drukt 1 af: je hebt nog voor geen van beide programma’s een eigen test geschreven. Dat is les 4.
Opdracht
Schrijf in scoreboard_client.py twee main weigert al een slechte client_parser, read_token en format_snapshot zoals ze zijn.
watch(url, max_messages, wait, *, connect=connect): maak verbinding metconnect(url, open_timeout=WAIT, close_timeout=WAIT, proxy=None)in eenwith-blok; roep voor elk berichtconnection.recv(timeout=wait)aan; druk een snapshot alleen af metformat_snapshotals zijn versie groter is dan de laatst afgedrukte, en geef 0 terug namax_messagesregels. Sla een geldigeerror-envelop over. Roep voor alles watscoreboard_messages.parse_messageweigert, en voor eenscore-envelop,connection.close(1008, ...)aan en geef daarna 1 terug. Geef 0 terug voor een close met 1000 of 1001, en 1 voor elke andere close, voorexc.rcvdgelijk aanNone, voorInvalidStatus,TimeoutErroren elke andereOSError.score(url, token, team, points, *, connect=connect): stuur één score-envelop, gebouwd metscoreboard_messages.encode_message, op een verbinding die is geopend metadditional_headers={"Authorization": f"Bearer {token}"},open_timeout=WAIT,close_timeout=WAITenproxy=None; lees één antwoord metrecv(timeout=WAIT). Druk een snapshot als antwoord af metformat_snapshoten geef 0 terug. Geef 1 terug voor eenerror-envelop,InvalidStatus,ConnectionClosed,TimeoutErroren elke andereOSError.
Elke keer dat 1 wordt teruggegeven, wordt er precies één regel naar standard error geschreven, en de regel van een geweigerde exc.response.status_code. Standard output bevat alleen snapshotregels. Geen van beide functies drukt ooit het token af of zet het in de
Draai python pl_ws.py client-main en python -m pytest -q. Klik daarna op Check my work. Die draait de controles van les 1 en 2 opnieuw, en roept daarna de meegeleverde main aan met een stand-in-connect waarvan de verbinding gescripte berichten uitdeelt, elke time-out van recv vastlegt en send en close vastlegt; geweigerde handshakes zijn de eigen InvalidStatus van de bibliotheek. watch --max-messages 3 --wait 1.5 die versies 5, 4, 5, 6, 7, 8 krijgt, moet versies 5, 6 en 7 afdrukken en 0 teruggeven, met proxy=None en het wachten bij elke recv. Elk einde in de tabel van deze les moet zijn exitstatus geven, met precies één regel op standard error voor elke 1 en een close met 1008 voor een bericht dat het protocol breekt. score moet verbinding maken met de opgegeven URL, precies één Authorization-Bearer en een token dat je nog niet hebt gezien, één score sturen waarvan de Headers zijn, met de naam in hoofd- of kleine letters, en de time-out mag op positie worden meegegeven.