Verzoeken met de hand schrijven · eindproject
Eindproject: Project: elk verzoek bouwen
Je hebt nu elk onderdeel van een verzoek: een urlsplit uit elkaar is gehaald, een doel gecodeerd met make_target, een Host-regel van host_header, en format_headers. Wat ontbreekt, is één fetch die haar gebruikt. Dat is dit project: build_request(method, url, headers=None, body=b""), en een fetch die een volledige URL aanneemt. Daarmee kan hoofdstuk 4 wire.py ook richten op een server die je zelf schrijft, op een adres dat niet dat van practice-api is.
Een verzoek kan een body hebben
Hoofdstuk 1 las bodies in antwoorden, afgemeten met Content-Length. Een verzoek kan op dezelfde manier een body hebben: de kop, de lege regel, dan precies zoveel bytes als de regel Content-Length aankondigt. Een PUT die boek 7 op wat read_body aan de clientkant doet.
Content-Length telt bytes, geen tekens. De JSON-tekst {"title":"naïve"} is 17 tekens, en de ï er twee kost. build_request krijgt de body al als bytes, dus len(body) is de juiste telling; een client die de tekens van de tekst telde, zou één byte te weinig aankondigen, en de server zou één byte te vroeg stoppen met lezen.
Wanneer je de regel verstuurt, volgt uit wat de methode betekent:
Als de body niet leeg is, verstuur je
Content-Length, welke methode het ook is.Voor
POSTenPUTverstuur je hem ook als de body leeg is, alsContent-Length: 0. Deze methoden dragen inhoud, dus een server die er een leest, verwacht te horen hoe groot die is, en 0 vertelt hem dat er geen is.Voor
GET,HEADenDELETEmet een lege body verstuur je helemaal geenContent-Length. Deze methoden verwachten geen body, envraagt een client er geen aan te kondigen die er niet is.
De regels voor build_request
build_request geeft het hele verzoek terug als bytes. Lees de regels helemaal door voordat je begint:
Splits de URL met
urlsplit. Als het scheme niethttpis, werp jeop: wire.pyspreekt alleen gewone HTTP, en eenhttps-URL heeft de-laag nodig die hoofdstuk 9 beschrijft. Het doel is
parts.path, of"/"als het pad leeg is, zoals inhttp://books.test; dan"?"enparts.queryals de query niet leeg is. Het fragment valt weg. Kopieer het pad en de query precies zoals de URL ze heeft: ze zouden al gecodeerd moeten zijn, doormake_targetof door wie de URL schreef, en ze een tweede keer coderen zou van elke%een%25maken.De kop begint met
request_line(method, target), dat nog steeds een doel weigert met een spatie,\rof\nerin.Dan
host_header(parts.hostname, parts.port). Een URL die geennoemt, geeft parts.portalsNone, en deHost-regel heeft geen poort.parts.hostnameis de host in kleine letters, wat prima is voor namen en, de enige hosts die deze cursus gebruikt. Dan de headers van de aanroeper, via
format_headers.build_requestschrijftHostenContent-Lengthzelf, dus als de aanroeper een van beide meegeeft, in welke schrijfwijze ook, werp jeValueErrorop: tweeHost-regels of twee lengtes zouden de server laten raden welke hij moet geloven, dezelfde dubbelzinnigheid dieread_bodyuit hoofdstuk 1 in een antwoord weigert.Dan
Content-Length: <len(body)>als de regels hierboven dat zeggen.Dan de lege regel. Codeer de kop als ASCII en voeg de bodybytes erachter toe, ongewijzigd.
De volgorde van headers met verschillende namen bepaal je zelf; Host eerst zetten, zoals in les 4, is wat HTTP aanraadt.
fetch neemt een URL aan
fetch houdt alles wat het na het verbinden doet. Wat verandert, is waarmee het verbinding maakt en wat het verstuurt:
def fetch(method, url, *, headers=None, body=b"", connect=socket.create_connection, timeout=TIMEOUT):
Het bouwt het verzoek met build_request(method, url, headers, body) voordat het verbinding maakt, zodat een geweigerde URL nooit een verbinding opent. Het maakt verbinding met (parts.hostname, port), waarbij port parts.port is, of 80 als dat None is. Binnen het with-blok verstuurt het het verzoek en leest het het antwoord zoals eerst.
main volgt: hernoem het tweede build_parser van target naar url, geef het de standaardwaarde BASE_URL + HOOK_TARGET, en geef args.url door aan fetch. Dan haalt python wire.py zonder argumenten dezelfde pagina op als altijd, en verstuurt python wire.py HEAD http://practice-api:8080/v1/items/1 een HEAD naar die URL.
ADDRESS wordt niet meer door fetch gebruikt; make_request gebruikt het nog wel, en je kunt beide laten zoals ze zijn.
Je tests
fetch neemt nu een URL aan, dus de tests in tests/test_wire.py die wire.fetch("GET", "/v1/items/1", ...) aanroepen, mislukken totdat je ze er een geeft. Voeg bovenaan een constante toe, ITEM_URL = "http://practice-api:8080/v1/items/1", en geef die mee in plaats van het doel. Het adres dat de fake connect vastlegt, blijft ("practice-api", 8080), omdat de URL daarheen wijst.
Voeg daarna drie tests toe, elk een gewone test_-functie zonder
een verzoek met een body buiten ASCII, zoals
"café".encode("utf-8"), waarvan de regelContent-Lengthde bytes telt, en dat eindigt met de lege regel en de body;een URL met een poort en een fragment, waarvan de verzoekregel het pad en de query bevat maar geen
#, en waarvan deHost-regel de poort bevat;een
https-URL, diebuild_requestweigert metValueError. Schrijf hem zoalstest_a_close_inside_the_body_is_an_error, dat al in het bestand staat: roepwire.build_requestaan binnentry, doeuit de test in de tak except ValueError:, en werp na detryAssertionErrorop als er geen fout kwam.
Roep elke functie binnen elke test via de wire.build_request(...) en wire.fetch(...), en houd import wire zonder enige regel from wire import. Als je op Check my work drukt, voert het je tests uit tegen je eigen wire.py en daarna tegen versies met elk één gebroken regel, en een naam die met from is geïmporteerd, zou jouw versie vasthouden en de kapotte verbergen.
Zie het werken
Deze uitvoeringen bereiken practice-api. Eerst een item waarvan het id met make_target is gebouwd:
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'
Daarna een query gebouwd met urlencode, uit getallen, die de codering laat zoals ze zijn:
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'])"
Het doel dat het afdrukt, is /v1/items?page=1&page_size=2, het openingsdoel dat hoofdstuk 1 met de hand typte, en de status is 200.
Tot slot hetzelfde item nog eens, met de 1 geschreven als zijn percent-encoding, %31:
python -c "import wire; r = wire.fetch('GET', wire.BASE_URL + '/v1/items/%31', headers=wire.PRACTICE_HEADERS); print(r['status'], r['body'])"
Je zou 404 moeten zien met "route not found", waar het eerste commando het item vond als /v1/items/1: practice-api vergelijkt het pad byte voor byte en decodeert er niets in, dus %31 is een ander pad, ook al decodeert het tot 1. Codering is een belofte tussen de code die codeert en de code die decodeert; daarom codeert make_target alleen wat gecodeerd moet worden. Een cijfer hoeft dat nooit, dus make_target verstuurt 1, en de server vindt het item. Is dit antwoord in plaats daarvan het item, dan decodeerde practice-api het segment voordat hij het vergeleek; de bytes die je verstuurde, waren hoe dan ook dezelfde.
Geen van deze antwoorden wordt gecontroleerd. Ze laten je client werken tegen een echte server; je tests zijn het bewijs.
Voer je tests uit:
python -m pytest -q
Met build_request, de nieuwe fetch en de bijgewerkte tests op hun plaats meldt pytest 9 passed: de zes tests waarmee je begon en je eigen drie. Als dat zo is, druk je op Check my work.
Als een test mislukt en je niet ziet waarom, vraag Monty dan de mislukte assert te lezen naast de regel uit deze les die hij controleert, en te zeggen welke van de twee fout is, zonder het voor je op te lossen.
wire.py kan nu elk verzoek bouwen en versturen naar elke server met gewone HTTP, en elke regel ervan is van jou. Hoofdstuk 3 geeft de http.client en laat de beslissingen aan jou.
Opdracht
Maak wire.py af, zodat het elk verzoek kan versturen naar elke server met gewone
Schrijf build_request(method, url, headers=None, body=b""). Het geeft het verzoek terug als bytes:
splits
urlmeturlsplit, en werpop als het scheme niet httpis;het doel is het pad, of
"/"als het pad leeg is, dan"?"en de query als die er is, gekopieerd zoals deze heeft; het fragment wordt nooit verstuurd; de kop is
request_line(method, target), danhost_header(parts.hostname, parts.port), danformat_headers(headers), danContent-Length: <len(body)>als de body niet leeg is of dePOSTofPUTis, dan de lege regel, gecodeerd als ASCII, met de bodybytes er ongewijzigd achter;een header
HostofContent-Lengthvan de aanroeper, in welke schrijfwijze ook, werptValueErrorop.
Verander fetch in fetch(method, url, *, headers=None, body=b"", connect=socket.create_connection, timeout=TIMEOUT): bouw eerst het verzoek met build_request, maak verbinding met de host en connect((host, port), timeout=timeout), verstuur het verzoek en lees het antwoord zoals eerst. Hernoem in build_parser het tweede url met de standaardwaarde BASE_URL + HOOK_TARGET, en geef in main args.url door aan fetch.
Geef in tests/test_wire.py de tests die wire.fetch aanroepen een volledige URL, zoals "http://practice-api:8080/v1/items/1", en voeg drie gewone test_-Content-Length bytes telt; een URL met een poort en een fragment, waarvan de verzoekregel geen # heeft en de Host-regel de poort; en een https-URL die met ValueError wordt geweigerd. Houd import wire, roep binnen elke test wire.build_request(...) en wire.fetch(...) aan, en voeg geen regel from wire import toe.
Voer python -m pytest -q uit in de
Druk daarna op Check my work. De controle roept build_request aan met methoden, URL’s, headers en bodies die je niet hebt gezien: een POST met een poort, een query, een fragment en een body met multibyte-tekens; bytes die geen tekst zijn; elk geval van Content-Length; URL’s met een leeg pad, een query zonder pad, en gecodeerde segmenten; herhaalde headerwaarden; en zes verzoeken die het moet weigeren. Ze roept fetch aan met URL’s die niet van practice-api zijn, via een vervangende connect-functie, met het antwoord op elke positie geknipt op een verbinding die sluit of openblijft, en controleert het adres, poort 80, de time-out, de headers en de body die het verstuurde. Ze roept main aan met fetch vervangen, om te zien welke URL het doorgeeft. Daarna voert ze je tests uit tegen je eigen wire.py, waar ze allemaal moeten slagen en er minstens negen verzameld moeten worden, en tegen drie versies waarvan build_request elk één regel breekt: een telt Content-Length in tekens, een houdt het fragment in het doel, en een accepteert een https-URL. Minstens een van je tests moet tegen elk ervan mislukken. De volgorde van headers met verschillende namen wordt niet gecontroleerd. Ze neemt nooit contact op met practice-api en leest je terminal niet.