0%

requests und httpx, jetzt, wo du Bescheid weißt · Abschlussprojekt

Abschlussprojekt: Projekt: Der Leselisten-Client

Seit Kapitel 1 war dein Client wire.py, für Funktion gebaut, damit jeder Mechanismus sichtbar blieb. Dieses Projekt baut den Client, den ein Mensch tatsächlich benutzen würde, mit requests, in reading_client.py:

python reading_client.py add --base-url URL --token TOKEN --title Piranesi --author "Susanna Clarke"
python reading_client.py show 3 --base-url URL

Hier führt pl_http die Befehle für dich gegen deinen Server aus. In Kapitel 12 führst du beides auf deinem eigenen Computer aus.

Was der Client tun muss

SERVICE_CONTRACT.md hat einen neuen Abschnitt, “The client”. Lies ihn zuerst; er ist die Anforderung. Kurz gesagt: fünf Befehle, list, show ID, add, update ID und delete ID, jeder davon eine Anfrage an den Dienst; --token für Schreibzugriffe; ein Timeout von TIMEOUT, (1.0, 2.0), bei jeder Anfrage; Ergebnisse auf der Standardausgabe; und ein Exit-Status, der sagt, was passiert ist: 0, wenn die Anfrage gelungen ist, 1, wenn keine Antwort kam, 2, wenn der Server mit einem Fehler geantwortet hat oder die gegen die Regeln verstößt.

reading_client.py enthält schon die Teile, bei denen es nicht um geht. build_parser() und check_usage(parser, args) lesen und prüfen die Kommandozeile, book_url(base_url, book_id) hängt /books oder /books/ID an die Basis-, und book_line(book) formatiert ID: TITLE by AUTHOR. Dein make_session() aus Lektion 1 ist auch da. Vier Funktionen bleiben übrig.

Eine Funktion sendet jede Anfrage

Jeder Befehl ist eine Anfrage mit ein paar Teilen, also sendet eine Funktion sie alle:

def call(method, url, *, session=None, token=None, data=None, timeout=TIMEOUT):

Benutze client = requests if session is None else session, wie in Lektion 2, und sende mit client.request(method, url, ...), das die als Text nimmt. Füge dann nur hinzu, was die Anfrage braucht. Mit einem Token einen Header authorization: Bearer TOKEN, gebaut aus dem Token, das du bekommen hast, und ohne Token keinen solchen Header. Mit Daten json=data, womit requests das codiert und content-type: application/json hinzufügt; Lesezugriffe senden keinen Body. Immer timeout=timeout.

brauchen nichts von dir. requests folgt ihnen, wie Lektion 2 gezeigt hat, und die alten /v0-Pfade des Dienstes beantworten jede Methode mit 308, was Methode und Body beibehält. Ein PUT über --base-url http://books.test/v0 kommt bei /books/ID als PUT an, mit seinem Body und seinem authorization-Header, weil requests diesen Header behält, wenn die Weiterleitung auf demselben Host bleibt.

Zuerst der Status, dann die Ausgabe

Eine Antwort ist anhand ihrer Klasse ein Erfolg oder ein Fehler, die Regel aus Kapitel 3: 2xx ist ein Ergebnis, das ausgegeben wird, und jede 4xx oder 5xx ist ein Fehler. Entscheide das, bevor du irgendetwas ausgibst. Bei einem Fehler gib eine Zeile auf der Standardfehlerausgabe aus, print(describe_error(response), file=sys.stderr), gib nichts auf der Standardausgabe aus und gib 2 zurück. Die Standardausgabe trägt nur Ergebnisse, damit ein Programm, das sie liest, nie eine Fehlermeldung für ein Buch hält.

describe_error(response) gibt diese eine Zeile zurück: den Status, ein Leerzeichen und dann den error-Text des Servers, wenn der Body ein - mit einem solchen Text ist, oder sonst die Reason Phrase. Eine 503 von einem Proxy hat vielleicht gar keinen JSON-Body, also kann response.json() eine auslösen; es löst requests.JSONDecodeError aus, eine Unterklasse von ValueError, die du mit except ValueError: abfängst.

delete 7 bekommt eine 403. Was tut der Client?

Wenn keine Antwort kommt, gibt es keine Antwort, die man ansehen könnte: requests löst stattdessen eine Ausnahme aus. Zwei Klassen sind wichtig, und ihre Reihenfolge ist wichtig. Ein requests.ConnectTimeout ist sowohl ein requests.Timeout als auch ein requests.ConnectionError, und Python führt den ersten passenden except-Zweig aus. Fange also zuerst requests.Timeout ab und gib no answer from BASE_URL in time aus, dann requests.ConnectionError und gib could not connect to BASE_URL aus, jeweils auf der Standardfehlerausgabe und jeweils mit 1. Fange nichts Breiteres ab. Ein except Exception: würde einen Fehler in deinem eigenen Code als Netzwerkproblem melden.

Warum requests.Timeout vor requests.ConnectionError abfangen?

Meilensteine

  1. call, wie oben. Der Beispieltest in tests/test_client.py besteht, sobald es existiert.

  2. describe_error(response), wie oben.

  3. run(args, *, session) führt einen geprüften Befehl aus. Wähle die Methode für args.command, baue für add und update {"title": args.title, "author": args.author}, sende es mit call(..., session=session, token=args.token, data=data) und prüfe zuerst den Status. Bei Erfolg gibt list für jedes Buch in response.json()["books"] ein book_line aus, show gibt die Zeile des Buchs aus, add und update setzen added oder updated davor, und delete gibt deleted ID aus; eine 204 hat keinen Body, also liest delete keinen. Gib 0 zurück.

  4. main(argv=None, *, session=None) parst mit build_parser(), ruft check_usage(parser, args) auf und führt den Befehl innerhalb von try aus. Ohne Session erstellt es eine, with make_session() as session:, damit die Session geschlossen wird, wenn der Befehl endet. Insgesamt etwa vierzig Zeilen.

Eine Kommandozeile, die der Vertrag ablehnt, erreicht run nie. Probiere eine im aus und frage die Shell dann nach dem Exit-Status des letzten Befehls:

python reading_client.py show --base-url http://books.test
echo $?

argparse gibt die Verwendung und reading_client.py: error: show needs a book id auf der Standardfehlerausgabe aus, und echo $? gibt 2 aus. Es wird keine Anfrage gesendet, also wird kein Server gebraucht.

Führe dann das Szenario aus:

python pl_http.py client

Es führt acht Befehle gegen deinen Server aus, auf einer Session, die pl_http dort einhängt. Das Update mit dem Token eines Lesers geht über den alten Pfad, wird weitergeleitet und abgelehnt:

request 1  PUT /v0/books/3  ->  308 Permanent Redirect  (connection 1)
request 2  PUT /books/3  ->  403 Forbidden  (connection 1)
--- your reading_client.py update 3 --base-url http://books.test/v0 --token tok-ben-reads-K9pz --title "Piranesi (paperback)" --author "Susanna Clarke" ---
exit status: 2
standard output: []
standard error: ['403 this token may read the list but not change it']

Der letzte Befehl benutzt eine zweite Session, deren Handler auf einen zeigt, an dem nichts lauscht. Auf einem Mac aufgezeichnet, kam die Ablehnung sofort und ergab standard error: ['could not connect to http://books.test'] mit Exit-Status 1; wenn ein System die Verbindung stattdessen warten ließe, erschiene deine Timeout-Zeile.

Führe auch deine Tests aus, jetzt, wo call existiert:

python -m pytest -q

Eigene Tests zu tests/test_client.py hinzuzufügen lohnt sich. fakes.fake_session antwortet genauso leicht mit einem Fehler wie mit einem Buch, und das capsys von pytest zeigt, was main ausgegeben hat:

def test_a_404_exits_2(capsys):
    calls = []
    session = fakes.fake_session([fakes.fake_response(404, {"error": "no book has that id"})], calls)
    status = reading_client.main(["show", "9", "--base-url", "http://books.test"], session=session)
    assert status == 2
    assert capsys.readouterr().out == ""

Klicke auf Check my work. Dein Client erledigt jetzt die Client-Hälfte von allem, was der Server von Hand tut: Weiterleitungen, Zugangsdaten, Framing und die Verbindung. Kapitel 11 wendet sich der anderen Seite zu und fragt, was ein Web-Framework für deinen Server tun würde.

Aufgabe

Baue reading_client.py, einen Kommandozeilen-Client für den Leselisten-Dienst, so wie SERVICE_CONTRACT.md ihn unter “The client” beschreibt.

Schreibe vier ; behalte die mitgelieferten build_parser, check_usage, book_url, book_line und dein make_session:

  1. call(method, url, *, session=None, token=None, data=None, timeout=TIMEOUT) sendet eine Anfrage mit client = requests if session is None else session und gibt die Antwort zurück. Es sendet authorization: Bearer TOKEN nur, wenn token angegeben ist, json=data nur, wenn data angegeben ist, und immer timeout=timeout. Es übergibt nie allow_redirects.

  2. describe_error(response) gibt eine Zeile zurück: den Status, ein Leerzeichen und dann den error-Text des Servers, oder die Reason Phrase, wenn der Body keinen hat.

  3. run(args, *, session) sendet die Anfrage des Befehls über call mit args.token, sendet für add und update {"title": ..., "author": ...} und prüft zuerst den Status: Bei jeder 4xx oder 5xx gibt es describe_error(response) auf der Standardfehlerausgabe aus, gibt nichts auf der Standardausgabe aus und gibt 2 zurück. Sonst gibt es die Ergebniszeile oder -zeilen aus dem Vertrag auf der Standardausgabe aus und gibt 0 zurück.

  4. main(argv=None, *, session=None) parst argv mit build_parser(), ruft check_usage(parser, args) auf und führt den Befehl aus, auf session, wenn eine übergeben wird, und sonst innerhalb von with make_session() as session:. Es fängt zuerst requests.Timeout ab, dann requests.ConnectionError, gibt für beide eine Zeile auf der Standardfehlerausgabe aus und gibt 1 zurück. Es fängt nichts Breiteres ab.

Im führt python pl_http.py client acht Befehle gegen deinen Server aus, zwei davon über seine alten /v0-Pfade, und zeigt für jeden Befehl Exit-Status, Standardausgabe und Standardfehlerausgabe. python -m pytest -q führt deine Tests aus.

Klicke dann auf Check my work. Mit einer Basis-, Büchern, Titeln und Tokens, die du noch nicht gesehen hast, führt es jeden Befehl über eine Session aus, die jeden Aufruf aufschreibt und aus einer antwortet: die , die URL, den -Body, den authorization-Header nur mit Token und den Timeout jeder Anfrage; die genauen Ausgabezeilen; eine Zeile auf der Standardfehlerausgabe, die den Status nennt, und Exit-Status 2 bei Antworten 404, 409, 500 und 503; Exit-Status 1 bei einem Timeout beim Verbindungsaufbau, einem Timeout beim Lesen und einer abgelehnten Verbindung, wobei ein Timeout anders gemeldet wird als eine abgelehnte Verbindung; eine gewöhnliche , die nicht abgefangen wird; und den Status 2 von argparse für eine , die der Vertrag ablehnt. Dann führt es add, update mit dem Token eines Lesers und mit dem eines Schreibers und delete über die alten /v0-Pfade eines kleinen Servers innerhalb der Prüfung im Speicher aus, dazu show für das gelöschte Buch, und prüft, dass main ohne Session eine erstellt und sie schließt. Zuletzt führt es deine Tests einmal aus; sie müssen alle bestehen. Nichts aus deinem Terminal wird bewertet.