0%

Verzoeken met de hand schrijven · eindproject

Eindproject: Project: elk verzoek bouwen

Je hebt nu elk onderdeel van een verzoek: een , gekozen om wat ze belooft, een die met urlsplit uit elkaar is gehaald, een doel gecodeerd met make_target, een Host-regel van host_header, en van format_headers. Wat ontbreekt, is één die ze samenbrengt voor welk verzoek dan ook, en een 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 zet, verstuurt die JSON als body. De server leest de kop, vindt de lengte en leest precies zoveel bytes, zoals je read_body aan de clientkant doet.

Content-Length telt bytes, geen tekens. De JSON-tekst {"title":"naïve"} is 17 tekens, en de ervan is 18 bytes, omdat ï 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 POST en PUT verstuur je hem ook als de body leeg is, als Content-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, HEAD en DELETE met een lege body verstuur je helemaal geen Content-Length. Deze methoden verwachten geen body, en vraagt 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:

  1. Splits de URL met urlsplit. Als het scheme niet http is, werp je op: wire.py spreekt alleen gewone HTTP, en een https-URL heeft de -laag nodig die hoofdstuk 9 beschrijft.

  2. Het doel is parts.path, of "/" als het pad leeg is, zoals in http://books.test; dan "?" en parts.query als 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, door make_target of door wie de URL schreef, en ze een tweede keer coderen zou van elke % een %25 maken.

  3. De kop begint met request_line(method, target), dat nog steeds een doel weigert met een spatie, \r of \n erin.

  4. Dan host_header(parts.hostname, parts.port). Een URL die geen noemt, geeft parts.port als None, en de Host-regel heeft geen poort. parts.hostname is de host in kleine letters, wat prima is voor namen en , de enige hosts die deze cursus gebruikt.

  5. Dan de headers van de aanroeper, via format_headers. build_request schrijft Host en Content-Length zelf, dus als de aanroeper een van beide meegeeft, in welke schrijfwijze ook, werp je ValueError op: twee Host-regels of twee lengtes zouden de server laten raden welke hij moet geloven, dezelfde dubbelzinnigheid die read_body uit hoofdstuk 1 in een antwoord weigert.

  6. Dan Content-Length: <len(body)> als de regels hierboven dat zeggen.

  7. 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 in 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 regel Content-Length de 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 de Host-regel de poort bevat;

  • een https-URL, die build_request weigert met ValueError. Schrijf hem zoals test_a_close_inside_the_body_is_an_error, dat al in het bestand staat: roep wire.build_request aan binnen try, doe uit de test in de tak except ValueError:, en werp na de try AssertionError op als er geen fout kwam.

Roep elke functie binnen elke test via de aan, als 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 , de verzoekregel en de framing aan Python’s eigen 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 url met urlsplit, en werp op als het scheme niet http is;

  • het doel is het pad, of "/" als het pad leeg is, dan "?" en de query als die er is, gekopieerd zoals de ze heeft; het fragment wordt nooit verstuurd;

  • de kop is request_line(method, target), dan host_header(parts.hostname, parts.port), dan format_headers(headers), dan Content-Length: <len(body)> als de body niet leeg is of de POST of PUT is, dan de lege regel, gecodeerd als ASCII, met de bodybytes er ongewijzigd achter;

  • een header Host of Content-Length van de aanroeper, in welke schrijfwijze ook, werpt ValueError op.

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 van de URL (80 als de URL er geen noemt) met connect((host, port), timeout=timeout), verstuur het verzoek en lees het antwoord zoals eerst. Hernoem in build_parser het tweede naar 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_- zonder toe: een body buiten ASCII waarvan 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 , en als je wilt de live uitvoeringen uit de les.

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.