0%

Hoofdstuk 3 · oefening

De standaardclient en statusklassen

Laat http.client het socketwerk doen

wire.py kan elk verzoek versturen en elk antwoord lezen dat zijn lengte aankondigt, en je schreef elke stap ervan zelf. De standaardbibliotheek van Python heeft een die dezelfde stappen doet, http.client. Dit hoofdstuk geeft de bytes aan die module en laat de beslissingen aan jou: wat een status betekent, welke headers ertoe doen, en wat je doet als er helemaal geen antwoord komt. Deze les schrijft de eerste versie van get_json, dat één GET doet via http.client en een record teruggeeft zoals het record dat fetch teruggeeft.

Wat http.client voor je doet

Elk onderdeel van fetch heeft een tegenhanger in http.client:

Je code in wire.pyGeschreven inhttp.client
connect(address, timeout=timeout) in fetchhoofdstuk 1, les 3HTTPConnection(host, port, timeout=...)
build_requesthoofdstuk 2, les 5conn.request(method, target, headers=...)
read_head en parse_headhoofdstuk 1, les 3 en 4conn.getresponse()
read_bodyhoofdstuk 1, les 5 en 6response.read()

Niets van wat je leerde, verdwijnt. http.client schrijft dezelfde verzoekregel en Host-regel, stopt met het lezen van de kop bij de lege regel, leest precies Content-Length bodybytes, en leest geen body na HEAD. Het leest ook bodies die zonder Content-Length aankomen, die je read_body nog weigert en die hoofdstuk 9 uit elkaar haalt. Misschien heb je gehoord dat requests voortbouwt op een bibliotheek die urllib3 heet; urllib3 bouwt op zijn beurt voort op http.client, dus de module die je hier ontmoet, ligt onder het hulpmiddel dat je in Python in Practice gebruikte.

Eén verbinding, één time-out

Als je http.client.HTTPConnection aanroept, krijg je een verbindingsobject:

import http.client

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

Er is nog niets verbonden. De verbinding gaat open als het eerste verzoek vertrekt. De timeout werkt zoals de time-out die je aan socket.create_connection meegaf: hij begrenst het verbinden en daarna elke keer dat er op bytes wordt gewacht. Laat je hem weg, dan wacht de verbinding zo lang als de server wil.

Drie aanroepen vormen één uitwisseling:

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

request verstuurt het verzoek. getresponse wacht op de kop en geeft een antwoordobject terug: response.status is de code als int, response.reason de reason-tekst, en response.read() geeft de body terug als bytes. Komt de kop niet binnen de time-out aan, dan werpt getresponse TimeoutError op, dezelfde die je opwierpen.

Zie wat het verstuurt

set_debuglevel(1) laat een verbinding afdrukken wat ze verstuurt en ontvangt. Voer dit uit in de ; het is de uitwisseling hierboven met de debuguitvoer aangezet:

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()
"

De eerste uitvoerregel is het verzoek, 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'

Je zou Host: practice-api:8080 en Accept-Encoding: identity moeten zien in de regel send:, en een regel reply: met HTTP/1.1 200 OK, gevolgd door regels header: met onder meer Server, Date en Connection. Luidt de antwoordregel HTTP/1.0 en is er geen regel Server, dan antwoordde de dienst rechtstreeks; wat http.client verstuurde, is hoe dan ook hetzelfde.

http.client voegde twee regels toe die je niet schreef: de Host-regel die je host_header met de hand schreef, en Accept-Encoding: identity, dat de server vraagt de body niet te comprimeren. Je eigen header komt daarna. De laatste uitvoerregel is 200 OK en de bodybytes, beginnend met b'{"has_next":true,"items":[.

Een verbinding die je kunt vervangen

fetch nam connect=socket.create_connection aan, zodat een test er een fake socket aan kon geven. get_json doet hetzelfde met make_connection=http.client.HTTPConnection: een test geeft een vervanger mee, en de echte klasse is de standaardwaarde. Je roept make_connection(HOST, PORT, timeout=timeout) precies zo aan als je HTTPConnection zou aanroepen. HOST, PORT en PRACTICE_HEADERS staan al bovenaan wire.py.

Open fakes.py: er staan twee nieuwe in. scripted_response(status, reason, header_pairs, body) bouwt een antwoord met de status, headers en body die je kiest. fake_make_connection(response, calls) geeft een vervanger voor HTTPConnection terug waarvan de verbinding dat antwoord teruggeeft en elke stap als vastlegt in calls: "connect" met host, en time-out, "request" met wat er werd verstuurd, en "close". Les 6 gebruikt beide voor je eigen tests.

Sluiten op elk pad

Een verbinding houdt een socket vast totdat je conn.close() aanroept. Een socket van socket.create_connection kon je in een with-statement gebruiken, maar een HTTPConnection niet. contextlib.closing vult dat gat: het verpakt elk met een close, zodat with het kan gebruiken, en roept close() aan als het blok eindigt:

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

Het blok eindigt op dezelfde manier, of de code erin nu afloopt of een exception opwerpt. Als getresponse TimeoutError opwerpt, wordt close() toch uitgevoerd, en gaat de exception door naar je aanroeper. Lees de body binnen het blok: de verbinding sluiten kan het antwoord meesluiten, en dan geeft read() b"" terug in plaats van de body.

Waarom verpak je de verbinding in contextlib.closing?

De headers, zoals parse_head ze teruggaf

response.getheaders() geeft een paren terug, elk een naam en een , in de volgorde waarin de server ze stuurde en met de namen in de schrijfwijze van de server. Bouw dezelfde dict die parse_head bouwde: loop over de paren, neem de naam met pair[0] en zet hem in kleine letters, en voeg pair[1] toe aan de lijst voor die naam. Een header die herhaald wordt, houdt elke waarde, en een opzoeking hangt nooit af van de schrijfwijze die de server gebruikte.

Je opdracht

  1. Schrijf in wire.py de functie get_json(target, *, make_connection, timeout). Verstuur binnen with contextlib.closing(make_connection(HOST, PORT, timeout=timeout)) as conn: een GET voor target met headers=PRACTICE_HEADERS, haal het antwoord op, bouw de headerdict en lees de body. Geef een dict terug met "status", "reason", "headers" en "body".

  2. Probeer het op practice-api:

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

Je zou 'status': 200 moeten zien, een 'content-type' van ['application/json; charset=utf-8'], en dezelfde bodybytes als bij de debuguitvoering. Wat je hier ziet, is voor jou; niets uit de terminal wordt beoordeeld.

Druk daarna op Check my work. get_json geeft nu elke status terug die aankwam. De volgende les vraagt wat een client daarmee moet doen.

Opdracht

Schrijf de eerste versie van get_json(target, *, make_connection, timeout) in wire.py.

  • Open de verbinding met make_connection(HOST, PORT, timeout=timeout), binnen with contextlib.closing(...) as conn:, zodat ze op elk pad wordt gesloten.

  • Verstuur één GET voor target met conn.request, met headers=PRACTICE_HEADERS.

  • Haal het antwoord op met conn.getresponse() en lees de body met response.read(), allebei binnen het blok.

  • Geef een terug met "status" (een int), "reason", "headers" (elke naam in kleine letters gekoppeld aan een van zijn , gebouwd uit response.getheaders()) en "body" (bytes).

  • Laat elke , zoals TimeoutError, get_json verlaten; closing sluit de verbinding toch.

Voer python -c "import wire; print(wire.get_json('/v1/items?page=1&page_size=2'))" uit in de om een live record te zien.

Druk daarna op Check my work. Die roept get_json aan met doelen die je niet hebt gezien en met een vervanger voor make_connection die vastlegt waar je code om vroeg: dat het één keer verbinding maakte met practice-api op 8080 met de gegeven time-out (standaard 2.0), één GET verstuurde met de oefenheader, status, reason, body en headers teruggaf met namen in kleine letters en herhaalde namen bewaard, de body las voordat de verbinding sloot, en de verbinding sloot na een antwoord, na een TimeoutError en na een geweigerde verbinding, waarbij beide exceptions doorgelaten werden. Een get_json die zijn eigen HTTPConnection bouwt in plaats van make_connection aan te roepen, mislukt. Niets uit je terminal wordt beoordeeld.