0%

Kapitel 3 · Übung

Der Standard-Client und die Statusklassen

Lass http.client die Socket-Arbeit machen

wire.py kann jede Anfrage senden und jede Antwort lesen, die ihre Länge ankündigt, und du hast jeden Schritt davon geschrieben. Pythons Standardbibliothek hat ein , das dieselben Schritte erledigt, http.client. Dieses Kapitel übergibt ihm die Bytes und überlässt dir die Entscheidungen: was ein Status bedeutet, welche Header wichtig sind und was zu tun ist, wenn überhaupt keine Antwort kommt. Diese Lektion schreibt die erste Version von get_json, das ein GET über http.client ausführt und einen Datensatz zurückgibt wie den, den fetch zurückgibt.

Was http.client für dich tut

Jedes Teil von fetch hat ein Gegenstück in http.client:

Dein Code in wire.pyGeschrieben inhttp.client
connect(address, timeout=timeout) in fetchKapitel 1, Lektion 3HTTPConnection(host, port, timeout=...)
build_requestKapitel 2, Lektion 5conn.request(method, target, headers=...)
read_head und parse_headKapitel 1, Lektionen 3 und 4conn.getresponse()
read_bodyKapitel 1, Lektionen 5 und 6response.read()

Nichts von dem, was du gelernt hast, geht verloren. http.client schreibt dieselbe Anfragezeile und dieselbe Zeile Host, hört bei der Leerzeile auf, den Kopf zu lesen, liest genau Content-Length Body-Bytes und liest nach HEAD keinen Body. Es liest auch Bodys, die ohne Content-Length ankommen, die dein read_body noch ablehnt und die Kapitel 9 auseinandernimmt. Vielleicht hast du gehört, dass requests auf einer Bibliothek namens urllib3 aufbaut; urllib3 wiederum baut auf http.client auf, also liegt das Modul, das du hier kennenlernst, unter dem Werkzeug, das du in Python in der Praxis benutzt hast.

Eine Verbindung, ein Timeout

Ein Aufruf von http.client.HTTPConnection gibt dir ein Verbindungsobjekt:

import http.client

conn = http.client.HTTPConnection("practice-api", 8080, timeout=2.0)

Noch ist nichts verbunden. Die Verbindung öffnet sich, wenn die erste Anfrage hinausgeht. Das timeout funktioniert wie das, das du socket.create_connection übergeben hast: Es begrenzt den Verbindungsaufbau und danach jedes Warten auf Bytes. Lässt du es weg, wartet die Verbindung so lange, wie der Server will.

Drei Aufrufe ergeben einen Austausch:

conn.request("GET", "/v1/items?page=1&page_size=2", headers={"X-Practice-Name": "http-with-python"})
response = conn.getresponse()
body = response.read()

request sendet die Anfrage. getresponse wartet auf den Kopf und gibt ein Antwortobjekt zurück: response.status ist der Code als int, response.reason der Statustext, und response.read() gibt den Body als Bytes zurück. Wenn der Kopf nicht innerhalb des Timeouts ankommt, löst getresponse TimeoutError aus, dieselbe , die deine ausgelöst haben.

Sieh, was es sendet

set_debuglevel(1) bringt eine Verbindung dazu, auszugeben, was sie sendet und empfängt. Führe das im aus; es ist der Austausch von oben mit eingeschalteter Debug-Ausgabe:

python -c "
import http.client
conn = http.client.HTTPConnection('practice-api', 8080, timeout=2.0)
conn.set_debuglevel(1)
conn.request('GET', '/v1/items?page=1&page_size=2', headers={'X-Practice-Name': 'http-with-python'})
response = conn.getresponse()
print(response.status, response.reason, response.read())
conn.close()
"

Die erste Zeile der Ausgabe ist die Anfrage, als Bytes:

send: b'GET /v1/items?page=1&page_size=2 HTTP/1.1\r\nHost: practice-api:8080\r\nAccept-Encoding: identity\r\nX-Practice-Name: http-with-python\r\n\r\n'

Du solltest in der Zeile send: die Header Host: practice-api:8080 und Accept-Encoding: identity sehen, und eine Zeile reply: mit HTTP/1.1 200 OK, gefolgt von Zeilen header:, darunter Server, Date und Connection. Wenn die Zeile reply HTTP/1.0 lautet und es keine Zeile Server gibt, hat der Dienst direkt geantwortet; was http.client gesendet hat, ist in beiden Fällen dasselbe.

http.client hat zwei Zeilen hinzugefügt, die du nicht geschrieben hast: die Zeile Host, die dein host_header von Hand geschrieben hat, und Accept-Encoding: identity, das den Server bittet, den Body nicht zu komprimieren. Dein Header kommt danach. Die letzte Zeile der Ausgabe ist 200 OK und die Body-Bytes, beginnend mit b'{"has_next":true,"items":[.

Eine Verbindung, die du ersetzen kannst

fetch hat connect=socket.create_connection genommen, damit ein Test ihm einen Fake-Socket geben konnte. get_json macht dasselbe mit make_connection=http.client.HTTPConnection: Ein Test übergibt einen Ersatz, und die echte Klasse ist der Standardwert. Du rufst make_connection(HOST, PORT, timeout=timeout) genau so auf, wie du HTTPConnection aufrufen würdest. HOST, PORT und PRACTICE_HEADERS stehen schon oben in wire.py.

Öffne fakes.py: Es hat zwei neue . scripted_response(status, reason, header_pairs, body) baut eine Antwort mit dem Status, den Headern und dem Body, die du wählst. fake_make_connection(response, calls) gibt einen Ersatz für HTTPConnection zurück, dessen Verbindung diese Antwort zurückgibt und jeden Schritt als in calls aufzeichnet: "connect" mit Host, und Timeout, "request" mit dem, was gesendet wurde, und "close". Lektion 6 benutzt beide für deine eigenen Tests.

Auf jedem Weg schließen

Eine Verbindung hält einen Socket, bis du conn.close() aufrufst. Ein Socket von socket.create_connection konnte in einer with-Anweisung benutzt werden, eine HTTPConnection aber nicht. contextlib.closing schließt diese Lücke: Es verpackt jedes , das eine close hat, sodass with es benutzen kann, und ruft close() auf, wenn der Block endet:

with contextlib.closing(make_connection(HOST, PORT, timeout=timeout)) as conn:
    conn.request("GET", target, headers=PRACTICE_HEADERS)
    response = conn.getresponse()
    ...

Der Block endet auf dieselbe Weise, ob der Code darin fertig wird oder eine Ausnahme auslöst. Wenn getresponse TimeoutError auslöst, läuft close() trotzdem, und die Ausnahme geht weiter an deinen Aufrufer. Lies den Body innerhalb des Blocks: Das Schließen der Verbindung kann die Antwort mit schließen, und dann gibt read() b"" statt des Bodys zurück.

Warum verpackst du die Verbindung in contextlib.closing?

Die Header, wie parse_head sie zurückgegeben hat

response.getheaders() gibt eine von Paaren zurück, jedes ein Name und ein , in der Reihenfolge, in der der Server sie gesendet hat, und mit den Namen in der Schreibweise des Servers. Baue dasselbe Dict, das parse_head gebaut hat: Lauf in einer über die Paare, nimm den Namen mit pair[0] und schreibe ihn klein, und hänge pair[1] an die Liste für diesen Namen an. Ein Header, der sich wiederholt, behält jeden Wert, und ein Zugriff hängt nie davon ab, wie der Server den Namen geschrieben hat.

Deine Aufgabe

  1. Schreibe in wire.py die Funktion get_json(target, *, make_connection, timeout). Sende innerhalb von with contextlib.closing(make_connection(HOST, PORT, timeout=timeout)) as conn: ein GET für target mit headers=PRACTICE_HEADERS, hole die Antwort, baue das Header-Dict und lies den Body. Gib ein Dict mit "status", "reason", "headers" und "body" zurück.

  2. Probiere es an practice-api aus:

python -c "import wire; print(wire.get_json('/v1/items?page=1&page_size=2'))"

Du solltest 'status': 200 sehen, ein 'content-type' mit ['application/json; charset=utf-8'] und dieselben Body-Bytes wie beim Debug-Lauf. Was du hier siehst, ist für dich; nichts aus dem Terminal wird bewertet.

Drücke dann Check my work. get_json gibt jetzt jeden Status zurück, der angekommen ist. Die nächste Lektion fragt, was ein Client damit tun sollte.

Aufgabe

Schreibe die erste Version von get_json(target, *, make_connection, timeout) in wire.py.

  • Öffne die Verbindung mit make_connection(HOST, PORT, timeout=timeout), innerhalb von with contextlib.closing(...) as conn:, damit sie auf jedem Weg geschlossen wird.

  • Sende mit conn.request ein GET für target und übergib dabei headers=PRACTICE_HEADERS.

  • Hole die Antwort mit conn.getresponse() und lies den Body mit response.read(), beides innerhalb des Blocks.

  • Gib ein mit "status" (ein int), "reason", "headers" (jeder kleingeschriebene Name auf eine seiner , gebaut aus response.getheaders()) und "body" (Bytes) zurück.

  • Lass jede , etwa TimeoutError, aus get_json heraus; closing schließt die Verbindung trotzdem.

Führe im python -c "import wire; print(wire.get_json('/v1/items?page=1&page_size=2'))" aus, um einen echten Datensatz zu sehen.

Drücke dann Check my work. Die Prüfung ruft get_json mit Zielen auf, die du nicht gesehen hast, und mit einem Ersatz für make_connection, der aufzeichnet, worum dein Code gebeten hat: dass es sich einmal mit practice-api auf 8080 mit dem angegebenen Timeout verbunden hat (standardmäßig 2.0), ein GET mit dem Übungs-Header gesendet hat, Status, Statustext, Body und Header mit kleingeschriebenen Namen und behaltenen wiederholten Namen zurückgegeben hat, den Body gelesen hat, bevor die Verbindung geschlossen wurde, und die Verbindung nach einer Antwort, nach einem TimeoutError und nach einer abgelehnten Verbindung geschlossen hat, wobei beide Ausnahmen durchgelassen werden. Ein get_json, das seine eigene HTTPConnection baut, statt make_connection aufzurufen, fällt durch. Nichts aus deinem Terminal wird bewertet.