requests und httpx, jetzt, wo du Bescheid weißt · Abschlussprojekt
Abschlussprojekt: Projekt: Der Leselisten-Client
Seit Kapitel 1 war dein Client wire.py, 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
reading_client.py enthält schon die Teile, bei denen es nicht um 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-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
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 authorization: Bearer TOKEN, gebaut aus dem Token, das du bekommen hast, und ohne Token keinen solchen Header. Mit Daten json=data, womit requests das content-type: application/json hinzufügt; Lesezugriffe senden keinen Body. Immer timeout=timeout.
/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 response.json() eine 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 except Exception: würde einen Fehler in deinem eigenen Code als Netzwerkproblem melden.
Warum requests.Timeout vor requests.ConnectionError abfangen?
Meilensteine
call, wie oben. Der Beispieltest intests/test_client.pybesteht, sobald es existiert.describe_error(response), wie oben.run(args, *, session)führt einen geprüften Befehl aus. Wähle die Methode fürargs.command, baue füraddundupdate{"title": args.title, "author": args.author}, sende es mitcall(..., session=session, token=args.token, data=data)und prüfe zuerst den Status. Bei Erfolg gibtlistfür jedes Buch inresponse.json()["books"]einbook_lineaus,showgibt die Zeile des Buchs aus,addundupdatesetzenaddedoderupdateddavor, unddeletegibtdeleted IDaus; eine 204 hat keinen Body, also liestdeletekeinen. Gib 0 zurück.main(argv=None, *, session=None)parst mitbuild_parser(), ruftcheck_usage(parser, args)auf und führt den Befehl innerhalb vontryaus. 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
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 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 build_parser, check_usage, book_url, book_line und dein make_session:
call(method, url, *, session=None, token=None, data=None, timeout=TIMEOUT)sendet eine Anfrage mitclient = requests if session is None else sessionund gibt die Antwort zurück. Es sendetauthorization: Bearer TOKENnur, wenntokenangegeben ist,json=datanur, wenndataangegeben ist, und immertimeout=timeout. Es übergibt nieallow_redirects.describe_error(response)gibt eine Zeile zurück: den Status, ein Leerzeichen und dann denerror-Text des Servers, oder die Reason Phrase, wenn der Body keinen hat.run(args, *, session)sendet die Anfrage des Befehls übercallmitargs.token, sendet füraddundupdate{"title": ..., "author": ...}und prüft zuerst den Status: Bei jeder 4xx oder 5xx gibt esdescribe_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.main(argv=None, *, session=None)parstargvmitbuild_parser(), ruftcheck_usage(parser, args)auf und führt den Befehl aus, aufsession, wenn eine übergeben wird, und sonst innerhalb vonwith make_session() as session:. Es fängt zuerstrequests.Timeoutab, dannrequests.ConnectionError, gibt für beide eine Zeile auf der Standardfehlerausgabe aus und gibt 1 zurück. Es fängt nichts Breiteres ab.
Im 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-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 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.