0%

Hoofdstuk 10 · oefening

requests en httpx, nu je het weet

Wat een sessie onthoudt

In Python in Practice stuurde je één requests.get met een time-out, riep je raise_for_status() aan en decodeerde je de . Elke aanroep zoals die staat op zichzelf. In hoofdstuk 7 had je meer nodig voor twee verzoeken na elkaar: een login en een /me die daarvan wist. Je bouwde make_cookie_opener, een opener met een cookiejar, en stuurde in login_and_me beide verzoeken daardoorheen. In hoofdstuk 9 schreef je fetch_many, zodat meerdere verzoeken één verbinding konden delen.

Een requests.Session is die opener, maar dan goed gedaan. Het is één waar je elk verzoek doorheen stuurt, en het draagt wat het ene verzoek achterlaat mee naar het volgende. Dit hoofdstuk keert terug naar requests, en later httpx, nu je het mechanisme achter elke kunt benoemen.

import requests

with requests.Session() as session:
    session.post("http://books.test/login", auth=("ana", "correct horse"), timeout=(1.0, 2.0))
    reply = session.get("http://books.test/me", timeout=(1.0, 2.0))

session.post en session.get nemen dezelfde als requests.post en requests.get. Er is ook session.request("POST", url, ...), dat de als tekst neemt. auth=(user, password) schrijft de regel Authorization: Basic voor je; les 2 bekijkt hoe.

Wat een sessie bewaart

Drie dingen die een sessie bewaart doen hier ter zake, en elk ervan gaat mee van de ene aanroep naar de volgende.

. session.cookies is de cookiejar. Zijn klasse, RequestsCookieJar, is een subklasse van de http.cookiejar.CookieJar die je in hoofdstuk 7 gebruikte, en hij volgt dezelfde regels: hij leest elke Set-Cookie-regel in een antwoord en stuurt bij elk later verzoek de cookies mee die bij de host en het pad passen. Je kopieert nooit zelf een cookie.

Standaardheaders. session.headers bevat de headers die met elk verzoek meegaan. Een nieuwe sessie heeft er al vier, die requests ongevraagd toevoegt:

User-Agent: python-requests/VERSION
Accept-Encoding: gzip, deflate, zstd
Accept: */*
Connection: keep-alive

VERSION staat voor de versie van requests die hier geïnstalleerd is. De zstd staat erbij omdat de Python waarop deze lessen zijn gecontroleerd, 3.14, die compressie kan decoderen; op een oudere Python eindigt de regel bij deflate. Je voegt er iets aan toe zoals bij een : session.headers["accept"] = "application/json". De namen negeren hoofdletters, zoals headernamen dat doen, dus die regel vervangt Accept: */*. Les 5 vertelt waar de vier voor dienen. Wijs je een nieuwe dict toe aan session.headers, dan gooi je de vier weg; verander de dict die je al hebt.

Verbindingen. Een sessie houdt een pool van open verbindingen bij, één per server. Je server spreekt /1.1, dus na een antwoord blijft de verbinding open, en het volgende verzoek van de sessie aan dezelfde server gaat erover. Dat is de keep-alive uit hoofdstuk 9, voor je gedaan.

Welke van deze dingen draagt een requests.Session mee van de ene aanroep naar de volgende?

Wat een sessie niet bewaart

Een time-out. Session heeft helemaal geen time-outinstelling, en de voor de hand liggende gok mislukt stilletjes: session.timeout = 2 slaat een attribuut op dat requests nooit leest, en de volgende aanroep kan zonder grens wachten. De enige manier om een aanroep te begrenzen is timeout= op die aanroep, elke keer.

Deze cursus geeft TIMEOUT = (1.0, 2.0) mee, een paar, zoals in Python in Practice: hoogstens 1 seconde om te verbinden, daarna hoogstens 2 seconden voor elke leesactie. Beide bestanden van de leerling definiëren het bovenaan. Een functie die een argument timeout neemt, geeft het door met timeout=timeout.

session.timeout = 2, daarna session.get(url). Hoe lang kan de aanroep wachten?

Sluit hem

Een sessie houdt verbindingen vast, dus sluit hem als je klaar bent, met hetzelfde with-statement dat je voor bestanden en gebruikt: with make_session() as session: sluit de sessie wanneer het blok eindigt.

Eén detail van de bibliotheekversies die deze cursus draait: het sluiten van de sessie beëindigt een verbinding pas wanneer je programma geen antwoord meer vasthoudt dat over die verbinding binnenkwam. Een antwoord dat in een bewaard wordt, of een HTTPError die er een meedraagt, houdt zijn verbinding open tot het weg is, of tot de inactiviteitsgrens van 2 seconden van je server uit hoofdstuk 9 haar beëindigt. pl_http sluit de verbindingen van zijn sessies zelf, dus zijn runs wachten daar nooit op.

Je opdracht

Twee functies, in twee bestanden.

  1. In reading_client.py geeft make_session() een nieuwe requests.Session() terug waarvan de standaardheaders ook accept: application/json zeggen. Les 6 bouwt daarop de commandoregelclient.

  2. In side_by_side.py logt who_am_i(base_url, user, password, *, session, timeout=TIMEOUT) in en vraagt wie je bent, op de sessie die hij krijgt:

    • POST base_url + "/login" met auth=(user, password) en timeout=timeout;

    • response.raise_for_status(), zodat een geweigerde login HTTPError opwerpt voordat er iets anders wordt verstuurd;

    • GET base_url + "/me" met timeout=timeout;

    • geef response.json()["user"] terug.

Gebruik de sessie die je krijgt voor beide verzoeken. Maak geen andere en roep requests.post niet rechtstreeks aan: het cookie van de login leeft in de cookiejar van die sessie, en alleen die sessie stuurt het terug.

Voer daarna de helper uit:

python pl_http.py requests-session

pl_http start je server en geeft who_am_i een eigen sessie. Die sessie stuurt naar je server op deze machine via een handler die pl_http erop koppelt, dus de naam books.test wordt nooit opgezocht. Voordat je iets schrijft, eindigt elke aanroep in raised NotImplementedError: not written yet: Lesson 1. Daarna:

--- requests and replies ---
request 1  POST /login  ->  200 OK  (connection 1)
request 2  GET /me  (with a cookie line)  ->  200 OK  (connection 1)
--- your who_am_i() on pl_http's session ---
value: 'ana'
--- requests and replies ---
request 1  POST /login  ->  401 Unauthorized  (connection 2)
--- your who_am_i() on a second session, with a wrong password ---
raised HTTPError: 401 Client Error: Unauthorized for url: http://books.test/login
--- requests and replies ---
--- the cookies in the first session's jar ---
session: 'domain books.test, path /, HttpOnly'

Lees het van boven naar beneden. Het verzoek voor /me droeg een cookieregel die je nooit hebt geschreven; de cookiejar toont het cookie dat je server zette, met de attributen uit hoofdstuk 7. Beide verzoeken van de eerste sessie gingen over verbinding 1. De tweede sessie, met een verkeerd wachtwoord, opende verbinding 2, kreeg 401, en raise_for_status() maakte daar een HTTPError van, dus /me werd nooit gevraagd. Het resultaatblok eindigt met serve() returned after 2 connections; the server is closed.

In het transcript staat bij POST /login en bij GET /me allebei (connection 1). Waarom één verbinding voor twee verzoeken?

Klik op Check my work. Les 2 koppelt de rest van de functies van requests aan de code die je met de hand schreef, en vindt één plek waar auth= en je eigen Basic-header het oneens zijn.

Opdracht

Maak een sessie die om vraagt, en log in via de sessie die je krijgt.

  1. In reading_client.py geeft make_session() een nieuwe requests.Session() terug met accept: application/json toegevoegd aan zijn standaardheaders. Voeg het toe aan de headers die hij al heeft (session.headers["accept"] = "application/json"); vervang ze niet door een nieuwe .

  2. In side_by_side.py doet who_am_i(base_url, user, password, *, session, timeout=TIMEOUT) het volgende:

    • hij stuurt POST base_url + "/login" met auth=(user, password) en timeout=timeout;

    • hij roept raise_for_status() aan op dat antwoord, zodat een geweigerde login HTTPError opwerpt en er niets meer wordt verstuurd;

    • hij stuurt GET base_url + "/me" met timeout=timeout;

    • hij geeft de user uit de JSON van dat antwoord terug.

Stuur beide verzoeken op session, met session.post, session.get of session.request. Maak geen eigen sessie, roep requests.post of requests.get niet aan en geef zelf geen mee: de cookiejar van de sessie stuurt het logincookie terug. Laat import requests staan zoals het is.

In de draait python pl_http.py requests-session je who_am_i op een sessie die pl_http op je server koppelt, daarna op een tweede sessie met een verkeerd wachtwoord, en drukt de cookies in de cookiejar van de eerste sessie af.

Klik daarna op Check my work. Die roept make_session() aan en leest de headers van de sessie. Hij draait who_am_i met een gebruiker, een wachtwoord, een basis- en een time-out die je niet hebt gezien, op een echte requests-sessie waarvan de verzoeken in het geheugen beantwoord worden door een kleine server binnen de controle: de login moet slagen, /me moet het cookie meedragen, elk verzoek moet de time-out meedragen die je kreeg, en een verkeerd wachtwoord moet HTTPError opwerpen voordat /me gevraagd wordt. Hij draait who_am_i ook op een sessie die alleen elke aanroep opschrijft, om te zien dat de inloggegevens in auth= gaan en dat er geen cookie met de hand wordt meegegeven. Niets uit je terminal wordt beoordeeld.