0%

Kapitel 10 · Übung

requests und httpx, jetzt, wo du Bescheid weißt

Was eine Session sich merkt

In Python in der Praxis hast du ein einzelnes requests.get mit Timeout gesendet, raise_for_status() aufgerufen und das decodiert. Jeder solche Aufruf steht für sich allein. In Kapitel 7 brauchtest du für zwei Anfragen hintereinander mehr: ein Login und ein /me, das davon wusste. Du hast make_cookie_opener gebaut, einen Opener mit -Speicher, und in login_and_me beide Anfragen durch ihn gesendet. In Kapitel 9 hast du fetch_many geschrieben, damit sich mehrere Anfragen eine Verbindung teilen können.

Eine requests.Session ist dieser Opener, richtig gemacht. Sie ist ein einziges , durch das du jede Anfrage sendest, und sie trägt mit sich, was eine Anfrage für die nächste hinterlässt. Dieses Kapitel kehrt zu requests zurück und später zu httpx, jetzt, wo du den Mechanismus hinter jeder benennen kannst.

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 und session.get nehmen dieselben wie requests.post und requests.get. Außerdem gibt es session.request("POST", url, ...), das die als Text nimmt. auth=(user, password) schreibt die Authorization: Basic-Zeile für dich; Lektion 2 sieht sich an, wie.

Was eine Session behält

Drei Dinge, die eine Session behält, sind hier wichtig, und jedes wird von einem Aufruf zum nächsten mitgenommen.

Cookies. session.cookies ist der Cookie-Speicher. Seine Klasse, RequestsCookieJar, ist eine Unterklasse des http.cookiejar.CookieJar, den du in Kapitel 7 benutzt hast, und sie folgt denselben Regeln: Sie liest jede Set-Cookie-Zeile in einer Antwort und sendet die Cookies, die zu Host und Pfad passen, mit jeder späteren Anfrage. Du kopierst nie ein Cookie von Hand.

Standard-Header. session.headers enthält die Header, die mit jeder Anfrage gesendet werden. Eine neue Session hat schon vier, die requests ungefragt hinzufügt:

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

VERSION steht für die hier installierte Version von requests. Das zstd ist da, weil das Python, mit dem diese Lektionen geprüft wurden, 3.14, diese Kompression decodieren kann. Auf einem älteren Python endet die Zeile bei deflate. Du ergänzt sie wie ein : session.headers["accept"] = "application/json". Die Namen ignorieren Groß- und Kleinschreibung, wie Header-Namen es tun, also ersetzt diese Zeile Accept: */*. Lektion 5 sagt, wofür die vier da sind. Wenn du session.headers ein neues Dictionary zuweist, wirfst du die vier weg; ändere das, das du hast.

Verbindungen. Eine Session hält einen Pool offener Verbindungen, eine pro Server. Dein Server spricht /1.1, also bleibt die Verbindung nach einer Antwort offen, und die nächste Anfrage der Session an denselben Server geht über sie hinaus. Das ist die offene Verbindung (Keep-alive) aus Kapitel 9, für dich erledigt.

Was davon nimmt eine requests.Session von einem Aufruf zum nächsten mit?

Was eine Session nicht behält

Einen Timeout. Session hat überhaupt keine Timeout-Einstellung, und die naheliegende Vermutung scheitert lautlos: session.timeout = 2 speichert ein Attribut, das requests nie liest, und der nächste Aufruf kann unbegrenzt warten. Der einzige Weg, einen Aufruf zu begrenzen, ist timeout= bei genau diesem Aufruf, jedes Mal.

Dieser Kurs übergibt TIMEOUT = (1.0, 2.0), ein Paar, wie in Python in der Praxis: höchstens 1 Sekunde für den Verbindungsaufbau, dann höchstens 2 Sekunden für jeden Lesevorgang. Beide Dateien, die du bearbeitest, definieren es ganz oben. Eine Funktion, die ein Argument timeout nimmt, gibt es mit timeout=timeout weiter.

session.timeout = 2, dann session.get(url). Wie lange kann der Aufruf warten?

Schließe sie

Eine Session hält Verbindungen, also schließe sie, wenn du fertig bist, mit derselben with-Anweisung, die du für Dateien und benutzt: with make_session() as session: schließt die Session, wenn der Block endet.

Ein Detail der Bibliotheksversionen, mit denen dieser Kurs läuft: Das Schließen der Session beendet eine Verbindung erst, wenn dein Programm keine Antwort mehr festhält, die über sie gekommen ist. Eine Antwort, die in einer liegt, oder ein HTTPError, der eine enthält, hält ihre Verbindung offen, bis sie weg ist oder bis das Leerlauflimit deines Servers von 2 Sekunden aus Kapitel 9 sie beendet. pl_http schließt die Verbindungen seiner Sessions selbst, sodass seine Läufe nie darauf warten.

Deine Aufgabe

Zwei Funktionen in zwei Dateien.

  1. In reading_client.py gibt make_session() eine neue requests.Session() zurück, deren Standard-Header zusätzlich accept: application/json sagen. Lektion 6 baut den Kommandozeilen-Client darauf auf.

  2. In side_by_side.py meldet sich who_am_i(base_url, user, password, *, session, timeout=TIMEOUT) an und fragt, wer du bist, auf der Session, die es bekommt:

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

    • response.raise_for_status(), damit ein abgelehntes Login HTTPError auslöst, bevor irgendetwas anderes gesendet wird;

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

    • gib response.json()["user"] zurück.

Benutze für beide Anfragen die Session, die du bekommst. Erstelle keine weitere und rufe requests.post nicht direkt auf: Das Cookie vom Login lebt im Cookie-Speicher dieser Session, und nur diese Session sendet es zurück.

Führe dann das Hilfsprogramm aus:

python pl_http.py requests-session

pl_http startet deinen Server und gibt who_am_i eine eigene Session. Diese Session sendet an deinen Server auf diesem Rechner, über einen Handler, den pl_http auf ihr einhängt, sodass der Name books.test nie nachgeschlagen wird. Bevor du etwas schreibst, endet jeder Aufruf in raised NotImplementedError: not written yet: Lesson 1. Danach:

--- 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'

Lies es von oben nach unten. Die /me-Anfrage enthielt eine Cookie-Zeile, die du nie geschrieben hast; der Cookie-Speicher zeigt das Cookie, das dein Server gesetzt hat, mit den Attributen aus Kapitel 7. Beide Anfragen der ersten Session gingen über Verbindung 1 hinaus. Die zweite Session, mit einem falschen Passwort, hat Verbindung 2 geöffnet, eine 401 bekommen, und raise_for_status() hat daraus einen HTTPError gemacht, sodass /me nie angefragt wurde. Der Ergebnisblock endet mit serve() returned after 2 connections; the server is closed.

Im Protokoll sagen POST /login und GET /me beide (connection 1). Warum eine Verbindung für zwei Anfragen?

Klicke auf Check my work. Lektion 2 ordnet die übrigen Funktionen von requests dem Code zu, den du von Hand geschrieben hast, und findet eine Stelle, an der auth= und dein eigener Basic-Header nicht übereinstimmen.

Aufgabe

Erstelle eine Session, die nach fragt, und melde dich über die Session an, die du bekommst.

  1. In reading_client.py gibt make_session() eine neue requests.Session() zurück, deren Standard-Header um accept: application/json ergänzt sind. Ergänze die Header, die sie schon hat (session.headers["accept"] = "application/json"); ersetze sie nicht durch ein neues .

  2. In side_by_side.py macht who_am_i(base_url, user, password, *, session, timeout=TIMEOUT) Folgendes:

    • es sendet POST base_url + "/login" mit auth=(user, password) und timeout=timeout;

    • es ruft raise_for_status() auf dieser Antwort auf, damit ein abgelehntes Login HTTPError auslöst und nichts weiter gesendet wird;

    • es sendet GET base_url + "/me" mit timeout=timeout;

    • es gibt user aus dem JSON dieser Antwort zurück.

Sende beide Anfragen über session, mit session.post, session.get oder session.request. Erstelle keine eigene Session, rufe weder requests.post noch requests.get auf und übergib kein selbst: Der Cookie-Speicher der Session sendet das Login-Cookie zurück. Lass import requests so, wie es ist.

Im führt python pl_http.py requests-session dein who_am_i auf einer Session aus, die pl_http auf deinem Server einhängt, dann auf einer zweiten Session mit falschem Passwort, und gibt die Cookies im Cookie-Speicher der ersten Session aus.

Klicke dann auf Check my work. Es ruft make_session() auf und liest die Header der Session. Es führt who_am_i mit einem Benutzer, einem Passwort, einer Basis- und einem Timeout aus, die du noch nicht gesehen hast, auf einer echten requests-Session, deren Anfragen von einem kleinen Server innerhalb der Prüfung im Speicher beantwortet werden: Das Login muss gelingen, /me muss das Cookie enthalten, jede Anfrage muss den Timeout tragen, den du bekommen hast, und ein falsches Passwort muss HTTPError auslösen, bevor /me angefragt wird. Außerdem führt es who_am_i auf einer Session aus, die nur jeden Aufruf aufschreibt, um zu sehen, dass die Zugangsdaten in auth= stehen und kein Cookie von Hand übergeben wird. Nichts aus deinem Terminal wird bewertet.