Anfragen von Hand schreiben · Abschlussprojekt
Abschlussprojekt: Projekt: jede Anfrage bauen
Du hast jetzt jedes Teil einer Anfrage: eine urlsplit auseinandergenommene make_target kodiertes Ziel, eine Zeile Host von host_header und format_headers. Was fehlt, ist eine fetch, das sie benutzt. Das ist dieses Projekt: build_request(method, url, headers=None, body=b"") und ein fetch, das eine vollständige URL nimmt. Damit kann Kapitel 4 wire.py auch auf einen Server richten, den du selbst schreibst, auf einer Adresse, die nicht die von practice-api ist.
Eine Anfrage kann einen Body tragen
Kapitel 1 hat Bodys in Antworten gelesen, abgemessen mit Content-Length. Eine Anfrage kann auf dieselbe Weise einen Body tragen: den Kopf, die Leerzeile, dann genau so viele Bytes, wie ihre Zeile Content-Length ankündigt. Ein PUT, das Buch 7 auf etwas read_body es auf der Client-Seite tut.
Content-Length zählt Bytes, nicht Zeichen. Der JSON-Text {"title":"naïve"} hat 17 Zeichen, und seine ï zwei braucht. build_request bekommt den Body schon als Bytes, also ist len(body) die richtige Zahl; ein Client, der die Zeichen des Texts zählt, würde ein Byte zu wenig ankündigen, und der Server würde ein Byte zu früh aufhören zu lesen.
Wann die Zeile gesendet wird, folgt aus der Bedeutung der Methode:
Wenn der Body nicht leer ist, sende
Content-Length, egal bei welcher Methode.Bei
POSTundPUTsende es auch dann, wenn der Body leer ist, alsContent-Length: 0. Diese Methoden tragen Inhalt, also erwartet ein Server, der eine davon liest, dass ihm die Größe genannt wird, und 0 sagt ihm, dass es keinen gibt.Bei
GET,HEADundDELETEmit leerem Body sende überhaupt keinContent-Length. Diese Methoden erwarten keinen Body, undbittet einen Client, keinen anzukündigen, der nicht da ist.
Die Regeln für build_request
build_request gibt die ganze Anfrage als Bytes zurück. Lies die Regeln vollständig durch, bevor du anfängst:
Zerlege die URL mit
urlsplit. Wenn ihr Schema nichthttpist, löseaus: wire.pyspricht nur einfaches HTTP, und eine URL mithttpsbraucht die-Schicht, die Kapitel 9 beschreibt. Das Ziel ist
parts.path, oder"/", wenn der Pfad leer ist, wie beihttp://books.test; dann"?"undparts.query, wenn die Query nicht leer ist. Das Fragment fällt weg. Übernimm Pfad und Query genau so, wie die URL sie enthält: Sie sollten schon kodiert sein, vonmake_targetoder von wem auch immer die URL geschrieben hat, und sie ein zweites Mal zu kodieren würde jedes%in%25verwandeln.Der Kopf beginnt mit
request_line(method, target), das ein Ziel mit einem Leerzeichen,\roder\nweiterhin ablehnt.Dann
host_header(parts.hostname, parts.port). Eine URL, die keinennennt, ergibt parts.portalsNone, und die ZeileHosthat keinen Port.parts.hostnameist der Host in Kleinbuchstaben, was für Namen undin Ordnung ist, die einzigen Hosts, die dieser Kurs benutzt. Dann die Header des Aufrufers, über
format_headers.build_requestschreibtHostundContent-Lengthselbst, also löseValueErroraus, wenn der Aufrufer einen davon übergibt, in beliebiger Schreibweise: Zwei ZeilenHostoder zwei Längen würden den Server raten lassen, welcher er glauben soll, dieselbe Mehrdeutigkeit, dieread_bodyaus Kapitel 1 in einer Antwort ablehnt.Dann
Content-Length: <len(body)>, wenn die Regeln oben das sagen.Dann die Leerzeile. Kodiere den Kopf als ASCII und füge die Body-Bytes unverändert danach an.
Die Reihenfolge zwischen Headern mit verschiedenen Namen bleibt dir überlassen; Host an den Anfang zu setzen, wie in Lektion 4, ist das, was HTTP empfiehlt.
fetch nimmt eine URL
fetch behält alles, was es nach dem Verbinden tut. Was sich ändert, ist, wohin es sich verbindet und was es sendet:
def fetch(method, url, *, headers=None, body=b"", connect=socket.create_connection, timeout=TIMEOUT):
Es baut die Anfrage mit build_request(method, url, headers, body), bevor es sich verbindet, sodass eine abgelehnte URL nie eine Verbindung öffnet. Es verbindet sich mit (parts.hostname, port), wobei port gleich parts.port ist, oder 80, wenn das None ist. Im with-Block sendet es die Anfrage und liest die Antwort wie vorher.
main zieht nach: Benenne das zweite build_parser von target in url um, gib ihm den Standardwert BASE_URL + HOOK_TARGET und übergib args.url an fetch. Dann holt python wire.py ohne Argumente dieselbe Seite ab wie immer, und python wire.py HEAD http://practice-api:8080/v1/items/1 sendet ein HEAD an diese URL.
ADDRESS wird von fetch nicht mehr benutzt; make_request benutzt es noch, und du kannst beide so lassen, wie sie sind.
Deine Tests
fetch nimmt jetzt eine URL, also werden die Tests in tests/test_wire.py, die wire.fetch("GET", "/v1/items/1", ...) aufrufen, fehlschlagen, bis du ihnen eine gibst. Füge oben eine Konstante hinzu, ITEM_URL = "http://practice-api:8080/v1/items/1", und übergib sie anstelle des Ziels. Die Adresse, die das Fake-connect aufzeichnet, bleibt ("practice-api", 8080), weil die URL dorthin zeigt.
Füge dann drei Tests hinzu, jeder eine einfache test_-Funktion ohne
eine Anfrage mit einem Body außerhalb von ASCII, etwa
"café".encode("utf-8"), deren ZeileContent-Lengthseine Bytes zählt und die mit der Leerzeile und dem Body endet;eine URL mit einem Port und einem Fragment, deren Anfragezeile Pfad und Query enthält, aber kein
#, und deren ZeileHostden Port enthält;eine URL mit
https, diebuild_requestmitValueErrorablehnt. Schreibe ihn so, wietest_a_close_inside_the_body_is_an_error, das schon in der Datei steht, geschrieben ist: Rufewire.build_requestinnerhalb vontryauf, kehre im Zweigexcept ValueError:mitaus dem Test zurück und löse nach dem tryAssertionErroraus, wenn kein Fehler kam.
Rufe in jedem Test jede Funktion über das wire.build_request(...) und wire.fetch(...), und behalte import wire ohne jede Zeile from wire import. Wenn du Check my work drückst, führt es deine Tests gegen dein eigenes wire.py aus und dann gegen Versionen mit je einer kaputten Regel, und ein mit from importierter Name würde deine Version behalten und die kaputte verstecken.
Sieh es funktionieren
Diese Läufe erreichen practice-api. Zuerst ein Eintrag, dessen id mit make_target gebaut wird:
python -c "import wire; url = wire.BASE_URL + wire.make_target(['v1', 'items', '1']); r = wire.fetch('GET', url, headers=wire.PRACTICE_HEADERS); print(r['status'], r['body'])"
200 b'{"category":"color","id":1,"name":"blue","value":10}\n'
Als Nächstes eine Query, die mit urlencode aus Zahlen gebaut wird, die die Kodierung unverändert lässt:
python -c "import wire; target = wire.make_target(['v1', 'items'], {'page': 1, 'page_size': 2}); print(target); print(wire.fetch('GET', wire.BASE_URL + target, headers=wire.PRACTICE_HEADERS)['status'])"
Das Ziel, das ausgegeben wird, ist /v1/items?page=1&page_size=2, das Hook-Ziel, das Kapitel 1 von Hand eingetippt hat, und der Status ist 200.
Zum Schluss noch einmal derselbe Eintrag, wobei die 1 in ihrer Prozentkodierung geschrieben ist, %31:
python -c "import wire; r = wire.fetch('GET', wire.BASE_URL + '/v1/items/%31', headers=wire.PRACTICE_HEADERS); print(r['status'], r['body'])"
Du solltest 404 mit "route not found" sehen, wo der erste Befehl den Eintrag als /v1/items/1 gefunden hat: practice-api vergleicht den Pfad Byte für Byte und dekodiert nichts darin, also ist %31 ein anderer Pfad, auch wenn es zu 1 dekodiert. Kodierung ist ein Versprechen zwischen dem Code, der kodiert, und dem Code, der dekodiert; deshalb kodiert make_target nur, was kodiert werden muss. Eine Ziffer muss das nie, also sendet make_target die 1, und der Server findet den Eintrag. Wenn diese Antwort stattdessen der Eintrag ist, hat practice-api das Segment vor dem Vergleich dekodiert; die Bytes, die du gesendet hast, waren in beiden Fällen dieselben.
Keine dieser Antworten wird geprüft. Sie zeigen, wie dein Client gegen einen echten Server arbeitet; deine Tests sind der Beleg.
Führe deine Tests aus:
python -m pytest -q
Wenn build_request, das neue fetch und die aktualisierten Tests fertig sind, meldet pytest 9 passed: die sechs Tests, mit denen du angefangen hast, und deine drei. Wenn das so ist, drücke Check my work.
Wenn ein Test fehlschlägt und du nicht siehst, warum, bitte Monty, die fehlschlagende Zusicherung neben der Regel zu lesen, die sie in dieser Lektion prüft, und zu sagen, welche der beiden falsch ist, ohne es für dich zu korrigieren.
wire.py kann jetzt jede Anfrage bauen und an jeden Server mit einfachem HTTP senden, und jede Zeile davon ist deine. Kapitel 3 übergibt den http.client und überlässt dir die Entscheidungen.
Aufgabe
Stelle wire.py so fertig, dass es jede Anfrage an jeden Server mit einfachem
Schreibe build_request(method, url, headers=None, body=b""). Die
Zerlege
urlmiturlsplitund löseaus, wenn das Schema nicht httpist;das Ziel ist der Pfad, oder
"/", wenn der Pfad leer ist, dann"?"und die Query, wenn es eine gibt, übernommen so, wie diesie enthält; das Fragment wird nie gesendet; der Kopf ist
request_line(method, target), dannhost_header(parts.hostname, parts.port), dannformat_headers(headers), dannContent-Length: <len(body)>, wenn der Body nicht leer ist oder diePOSToderPUTist, dann die Leerzeile, kodiert als ASCII, mit den Body-Bytes unverändert danach;ein Header
HostoderContent-Lengthdes Aufrufers, in beliebiger Schreibweise, löstValueErroraus.
Ändere fetch zu fetch(method, url, *, headers=None, body=b"", connect=socket.create_connection, timeout=TIMEOUT): Baue die Anfrage zuerst mit build_request, verbinde dich mit connect((host, port), timeout=timeout) mit Host und build_parser das zweite url um, mit dem Standardwert BASE_URL + HOOK_TARGET, und übergib in main args.url an fetch.
Gib in tests/test_wire.py den Tests, die wire.fetch aufrufen, eine vollständige URL, etwa "http://practice-api:8080/v1/items/1", und füge drei einfache test_-Funktionen ohne Content-Length Bytes zählt; eine URL mit einem Port und einem Fragment, deren Anfragezeile kein # hat und deren Zeile Host den Port hat; und eine URL mit https, die mit ValueError abgelehnt wird. Behalte import wire, rufe in jedem Test wire.build_request(...) und wire.fetch(...) auf und füge keine Zeile from wire import hinzu.
Führe im python -m pytest -q aus, und wenn du möchtest, die Live-Läufe aus der Lektion.
Drücke dann Check my work. Die Prüfung ruft build_request mit Methoden, URLs, Headern und Bodys auf, die du nicht gesehen hast: ein POST mit einem Port, einer Query, einem Fragment und einem Body aus Mehrbyte-Zeichen; Bytes, die kein Text sind; jeden Fall von Content-Length; URLs mit leerem Pfad, mit einer Query und ohne Pfad und mit kodierten Segmenten; wiederholte Headerwerte; und sechs Anfragen, die sie ablehnen muss. Sie ruft fetch mit URLs, die nicht die von practice-api sind, über eine Ersatz-Funktion für connect auf, wobei die Antwort an jeder Position geschnitten wird, auf einer Verbindung, die schließt oder offen bleibt, und prüft die Adresse, Port 80, das Timeout, die Header und den Body, die es gesendet hat. Sie ruft main mit ersetztem fetch auf, um die URL zu sehen, die es übergibt. Dann führt sie deine Tests gegen dein eigenes wire.py aus, wo alle bestehen und mindestens neun gesammelt werden müssen, und gegen drei Versionen, deren build_request je eine Regel bricht: eine zählt Content-Length in Zeichen, eine behält das Fragment im Ziel, und eine akzeptiert eine URL mit https. Mindestens einer deiner Tests muss gegen jede davon fehlschlagen. Die Reihenfolge zwischen Headern mit verschiedenen Namen wird nicht geprüft. Sie nimmt nie Kontakt mit practice-api auf und liest dein Terminal nicht.