0%

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, oder , und ein Kommandozeilen-Client muss jedes davon in einen Exit-Status verwandeln, nach dem ein Mensch oder ein Skript handeln kann.

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 mit 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 endeteExit-Status
max_messages Zeilen ausgegeben0
ein Schließen mit 1000 oder 10010
ein Schließen mit jedem anderen Code1
die Verbindung endete ohne Close- (exc.rcvd ist None, die 1006 aus Kapitel 6)1
der wurde abgelehnt (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ßen1
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 lässt die Bibliothek die Verbindung schon selbst mit 1007 scheitern. Gültiges , das die Regeln der Anzeigetafel verletzt, ist eine Frage der Richtlinie dieses Protokolls, wie das eigene Schließen des Servers mit 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 gelesen, Bearer und das Token. Kapitel 9 hat auch ?token= in der akzeptiert, aber nur für eine Browserseite, die den Header nicht hinzufügen kann. Dieser Client ist kein Browser, und eine URL ist genau die Art von Text, die Server und Proxys in ihre Logs schreiben.

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 der Befehle ausgegeben:

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 , auf dem nichts lauscht, und beendet sich mit 1. Deine eigenen Zeilen in der Standardfehlerausgabe können es anders sagen. Führe dann die Tests aus und frag nach ihrem Exit-Status:

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 . Das vorgegebene main lehnt eine schlechte schon mit Exit-Status 2 und eine unlesbare Token-Datei mit 1 ab; lass es, client_parser, read_token und format_snapshot, wie sie sind.

  1. watch(url, max_messages, wait, *, connect=connect): Verbinde dich mit connect(url, open_timeout=WAIT, close_timeout=WAIT, proxy=None) in einem with-Block; rufe für jede Nachricht connection.recv(timeout=wait) auf; gib einen Snapshot nur dann mit format_snapshot aus, wenn seine Version größer ist als die zuletzt ausgegebene, und gib nach max_messages Zeilen 0 zurück. Überspringe einen gültigen error-Umschlag. Rufe für alles, was scoreboard_messages.parse_message ablehnt, und für einen score-Umschlag connection.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ür exc.rcvd gleich None, für InvalidStatus, TimeoutError und jeden anderen OSError.

  2. score(url, token, team, points, *, connect=connect): Sende einen Wertungs-Umschlag, gebaut mit scoreboard_messages.encode_message, auf einer Verbindung, die mit additional_headers={"Authorization": f"Bearer {token}"}, open_timeout=WAIT, close_timeout=WAIT und proxy=None geöffnet wurde; lies eine Antwort mit recv(timeout=WAIT). Gib eine Snapshot-Antwort mit format_snapshot aus und gib 0 zurück. Gib 1 zurück für einen error-Umschlag, InvalidStatus, ConnectionClosed, TimeoutError und jeden anderen OSError.

Jede Rückgabe von 1 schreibt genau eine Zeile in die Standardfehlerausgabe, und die Zeile eines abgelehnten nennt 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- senden, Bearer und ein Token, das du nicht gesehen hast, eine Wertung senden, deren dem übergebenen Team und den übergebenen Punkten entspricht, die Snapshot-Antwort ausgeben und für die Fehlschläge oben 1 zurückgeben, wobei es für einen abgelehnten Handshake 401 nennt. Das Token darf in keinem Ausgabestrom und in keiner URL erscheinen. Jeder Wortlaut in der Standardfehlerausgabe ist in Ordnung, der Header darf ein , eine von Paaren oder ein Headers sein, sein Name in beliebiger Groß- und Kleinschreibung, und das Timeout darf als Positionsargument übergeben werden.