Abschlussprojekt: Eine Live-Anzeigetafel · Abschlussprojekt
Abschlussprojekt: Die Client-CLI fertigstellen
Bisher hast du auf der Seite des Servers der Anzeigetafel gestanden und entschieden, was gesendet wird, wann abgelehnt wird und wie geschlossen wird. Diese Lektion stellt dich auf die andere Seite deines eigenen Protokolls. Alles, was der Server entscheidet, erreicht einen Client als Nachricht,
scoreboard_client.py ist halb geschrieben. client_parser() deklariert die beiden Befehle, watch und score, und ihre Optionen. Das vorgegebene main(argv=None, *, connect=connect) parst sie, lehnt eine schlechte parser.error ab (Exit-Status 2: --points außerhalb von 1 bis 10, ein leeres --team, ein --wait außerhalb seines Bereichs), liest für score die Token-Datei und gibt 1 mit einer Zeile in der Standardfehlerausgabe zurück, wenn es das nicht kann, und gibt dann zurück, was watch(...) oder score(...) zurückgibt. format_snapshot(snapshot) verwandelt einen Snapshot in eine Zeile, mit den Teams in der Reihenfolge, in der die Nachricht sie aufführt.
Was gibt das aus?
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)
Niemals rückwärts
watch(url, max_messages, wait, *, connect=connect) verbindet sich, gibt mit format_snapshot eine Zeile pro Snapshot aus und hört nach max_messages Zeilen auf. Es wendet die Regel aus der Lektion über das erneute Verbinden in Kapitel 8 an: Ein Client zeigt nie eine Tafel, die älter ist als eine, die er schon gezeigt hat. Behalte die Version des zuletzt ausgegebenen Snapshots, beginne unterhalb jeder echten Version (-1 funktioniert) und überspringe einen Snapshot, dessen Version nicht größer ist. Nur ausgegebene Zeilen zählen für --max-messages.
Der Server sendet Versionen schon in der richtigen Reihenfolge, aber der Client verlässt sich nicht allein auf den Server: Er hält die Regel ein, selbst wenn ein Server, ein Proxy oder eine spätere Version deines eigenen Codes es nicht tut. Nicht jede Nachricht ist ein Snapshot. Ein gültiger error-Umschlag ist für einen Punktezähler gedacht, also überspringt watch ihn. Ein score-Umschlag verletzt das Protokoll, weil nur der Punktezähler Wertungen sendet, und ebenso alles, was parse_message ablehnt.
watch hat Version 7 ausgegeben und empfängt dann einen Snapshot mit Version 6. Was tut es?
Wie es endete, entscheidet den Exit-Status
Jede Art, wie das Gespräch enden kann, hat im Vertrag einen Exit-Status. Für watch:
| Wie das Gespräch endete | Exit-Status |
|---|---|
max_messages Zeilen ausgegeben | 0 |
| ein Schließen mit 1000 oder 1001 | 0 |
| ein Schließen mit jedem anderen Code | 1 |
die Verbindung endete ohne Close-exc.rcvd ist None, die 1006 aus Kapitel 6) | 1 |
der InvalidStatus, mit dem Status in exc.response.status_code) | 1 |
kein Snapshot innerhalb von wait Sekunden oder keine rechtzeitige Antwort auf den Handshake (TimeoutError) | 1 |
dort lauscht nichts (ConnectionRefusedError oder ein anderer OSError) | 1 |
| eine Nachricht, die das Protokoll verletzt: zuerst mit 1008 schließen | 1 |
ein gültiger error-Umschlag | übersprungen |
Jedes Beenden mit 1 schreibt genau eine Zeile in die Standardfehlerausgabe, die sagt, warum, mit print(..., file=sys.stderr); die Standardausgabe enthält nur Snapshot-Zeilen. Ein abgelehnter Handshake nennt seinen Status, wie in HTTP 503.
Warum 1008 für eine schlechte Nachricht? RFC 6455, Abschnitt 7.4.1, macht 1008, Richtlinienverstoß, zum allgemeinen Code, wenn kein spezifischerer passt. 1003 ist für eine Art von Daten, die der Endpunkt nicht annehmen kann, etwa einen Binär-Frame, und bei ungültigem no such path.
Zwei Tatsachen entscheiden die Reihenfolge deiner except-Klauseln. TimeoutError ist eine Art OSError, also muss seine Klausel vor einem except OSError: kommen, das ihn sonst mit der falschen Meldung abfangen würde. Und InvalidStatus ist überhaupt kein OSError: Es ist eine WebSocketException, braucht also eine eigene Klausel. Übergib open_timeout, close_timeout und proxy=None an connect, wie in Kapitel 6, und übergib das Warten an jedes recv, damit kein stiller Server den Client für immer festhalten kann.
Warum muss except TimeoutError: in watch vor except OSError: kommen?
Das Token kommt in einen Header
score(url, token, team, points, *, connect=connect) sendet eine Wertung und meldet die Antwort. Das Token reist im Handshake, als zusätzlicher 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)
Auf einem lokalen Computer hat der process_request-Hook eines Servers aus einem so gesendeten Handshake genau einen Bearer und das Token. Kapitel 9 hat auch ?token= in der
Baue den Wertungs-Umschlag mit scoreboard_messages.encode_message, sende ihn einmal und lies eine Antwort mit recv(timeout=WAIT). Eine Snapshot-Antwort wird mit format_snapshot ausgegeben, Exit 0. Ein error-Umschlag, ein abgelehnter Handshake, ein Schließen vor der Antwort, ein Timeout oder keine Verbindung ergibt eine Zeile in der Standardfehlerausgabe, Exit 1. Die Zeile für einen abgelehnten Handshake nennt den Status, etwa HTTP 401, und nie das Token: Das Token erscheint in keinem Ausgabestrom.
watch verbindet sich nicht neu, wenn seine Verbindung abbricht. Das erneute Verbinden mit Zurückweichen aus Kapitel 8 bleibt etwas, das du wiedererkennen kannst, und nichts, was dieser Client tut: Er endet, und sein Exit-Status sagt, wie.
Warum sendet score das Token in einem Authorization-Header und nicht als ?token= in der URL?
Deine Aufgabe
Schreibe watch und score. Der Großteil von beiden besteht aus except-Klauseln. Führe dann sieben Befehle deines Clients gegen eine für diesen Kurs geschriebene Anzeigetafel aus, damit ein Fehler, den du siehst, der des Clients ist und nicht der deines Servers:
python pl_ws.py client-main
Eine fertige Version hat auf der Seite nach der
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
Zeilen mit | gingen in die Standardausgabe und Zeilen mit ! in die Standardfehlerausgabe. Befehl 6, --points 11, wird vom vorgegebenen main mit seiner Verwendung und Exit-Status 2 abgelehnt, und Befehl 7 schaut auf einen
python -m pytest -q
echo $?
Die sechs assert False-Tests scheitern weiterhin, also gibt echo $? 1 aus: Du hast noch für keines der beiden Programme einen eigenen Test geschrieben. Das kommt in Lektion 4.
Aufgabe
Schreibe in scoreboard_client.py zwei main lehnt eine schlechte client_parser, read_token und format_snapshot, wie sie sind.
watch(url, max_messages, wait, *, connect=connect): Verbinde dich mitconnect(url, open_timeout=WAIT, close_timeout=WAIT, proxy=None)in einemwith-Block; rufe für jede Nachrichtconnection.recv(timeout=wait)auf; gib einen Snapshot nur dann mitformat_snapshotaus, wenn seine Version größer ist als die zuletzt ausgegebene, und gib nachmax_messagesZeilen 0 zurück. Überspringe einen gültigenerror-Umschlag. Rufe für alles, wasscoreboard_messages.parse_messageablehnt, und für einenscore-Umschlagconnection.close(1008, ...)auf und gib dann 1 zurück. Gib 0 für ein Schließen mit 1000 oder 1001 zurück und 1 für jedes andere Schließen, fürexc.rcvdgleichNone, fürInvalidStatus,TimeoutErrorund jeden anderenOSError.score(url, token, team, points, *, connect=connect): Sende einen Wertungs-Umschlag, gebaut mitscoreboard_messages.encode_message, auf einer Verbindung, die mitadditional_headers={"Authorization": f"Bearer {token}"},open_timeout=WAIT,close_timeout=WAITundproxy=Nonegeöffnet wurde; lies eine Antwort mitrecv(timeout=WAIT). Gib eine Snapshot-Antwort mitformat_snapshotaus und gib 0 zurück. Gib 1 zurück für einenerror-Umschlag,InvalidStatus,ConnectionClosed,TimeoutErrorund jeden anderenOSError.
Jede Rückgabe von 1 schreibt genau eine Zeile in die Standardfehlerausgabe, und die Zeile eines abgelehnten exc.response.status_code. Die Standardausgabe enthält nur Snapshot-Zeilen. Keine der beiden Funktionen gibt jemals das Token aus oder legt es in die
Führe python pl_ws.py client-main und python -m pytest -q aus. Klicke dann auf Check my work. Die Prüfung führt die Prüfungen der Lektionen 1 und 2 erneut aus und ruft dann das vorgegebene main mit einem Stellvertreter-connect auf, dessen Verbindung gescriptete Nachrichten herausgibt, jedes recv-Timeout aufzeichnet und send und close aufzeichnet; abgelehnte Handshakes sind das eigene InvalidStatus der Bibliothek. watch --max-messages 3 --wait 1.5 mit den Versionen 5, 4, 5, 6, 7, 8 muss die Versionen 5, 6 und 7 ausgeben und 0 zurückgeben, mit proxy=None und dem Warten bei jedem recv. Jedes Ende in der Tabelle dieser Lektion muss seinen Exit-Status ergeben, mit genau einer Zeile in der Standardfehlerausgabe für jede 1 und einem Schließen mit 1008 für eine Nachricht, die das Protokoll verletzt. score muss sich mit der übergebenen URL verbinden, genau einen Authorization-Bearer und ein Token, das du nicht gesehen hast, eine Wertung senden, deren Headers sein, sein Name in beliebiger Groß- und Kleinschreibung, und das Timeout darf als Positionsargument übergeben werden.