0%

requests und httpx, jetzt, wo du Bescheid weißt · Übung

Jede Funktion ist ein Mechanismus

In Lektion 1 hat eine Session ein für dich mitgenommen. Jede andere von requests, die dieser Kurs benutzt, hast du auch schon von Hand geschrieben, in wire.py oder in deinem Server. Hier stehen sie nebeneinander:

requestsWas es tutDeine Version
response.historydie unterwegs gefolgten , die älteste zuerstfollow und seine history, Lektion 6.4
allow_redirects=Falsedie erste 3xx zurückgeben, statt ihr zu folgenhttp.client, das nie folgt, Lektion 3.1
auth=(user, password)die Authorization: Basic-Zeilebasic_header, Lektion 7.5
session.cookiesder Cookie-Speicher: Set-Cookie behalten, Cookie sendenmake_cookie_opener, Lektion 7.3
raise_for_status()bei einem Status 4xx oder 5xx eine auslösenclassify und seine Klassen für Client- und Serverfehler, Kapitel 3
der Verbindungspool der Sessionmehrere Anfragen über eine offen gehaltene Verbindungfetch_many, Lektion 9.4
.json()den Body decodieren, gelesen bis zu der Länge, die die Header angegeben habenread_body und json_body, Kapitel 1 und 3
timeout=(1.0, 2.0)ein begrenzter Verbindungsaufbau, dann begrenzte Lesevorgängedie Timeouts der Lektionen 3.1 und 3.5

Die Tabelle hat keine Zeile für einen Cache. Eine Antwort aufzubewahren, zu entscheiden, ob sie frisch ist, und If-None-Match zu senden, war dein cached_get in Lektion 8.5, und requests tut nichts davon. Mehr dazu weiter unten.

Nimm die ersten beiden Zeilen im Code. Nach response = session.get("http://books.test/v0/books/2") enthält response.history die 308-Antwort, und response.url ist die endgültige , http://books.test/books/2. Jede Antwort in history hat ihren eigenen status_code und ihre eigene url, dieselben zwei Fakten, die dein follow in seinen Einträgen der History behalten hat. Mit allow_redirects=False verhält sich requests wie http.client: Die 308 selbst kommt zurück, response.is_redirect ist True, und response.next ist die Anfrage, die es als Nächstes gesendet hätte.

Die meisten Zeilen verhalten sich genau wie dein Code. Drei nicht: die 302-Regel, der Standard bei head() und die Codierung, die auth= benutzt. Die solltest du kennen, bevor du der Bibliothek vertraust.

Wo die Bibliothek sich von dir unterscheidet

Dein follow ändert die nur nach einer 303 oder nach einer 301 oder 302 bei einem POST; nach einer 307 oder 308 behält es Methode und Body bei. requests stimmt bei 301, 307 und 308 zu, und bei 303 für jede Methode außer HEAD, die es beibehält. Bei 302 macht es etwas anderes: Es macht aus jeder Methode außer HEAD ein GET, also wird ein PUT, das mit 302 beantwortet wurde, als GET ohne Body weitergeschickt. Sein sagt sinngemäß, warum: Browser tun es, entgegen dem Standard. RFC 9110 erlaubt einem Client, nach einer 301 oder 302 nur ein POST in ein GET zu ändern. httpx, in Lektion 3, hat dieselbe Browser-Regel.

Das ist ein Grund, warum dein Server für die alten /v0-Pfade 308 benutzt: Bei einer 308 behält jeder Client Methode und Body bei.

Ein Server beantwortet PUT /books/2 mit 302 und location: /books/2-new. Was sendet requests als Nächstes?

Der andere Unterschied ist ein Standardwert. session.get, post, put und delete folgen Weiterleitungen, außer du übergibst allow_redirects=False. session.head tut das Gegenteil: Es gibt eine 3xx so zurück, wie sie ist, außer du übergibst allow_redirects=True.

session.head(url) trifft auf eine 308. Was kommt zurück?

Zugangsdaten sind Bytes

auth=(user, password) baut denselben Header wie dein basic_header: user:password, dann . Der Unterschied ist der Schritt dazwischen. Dein basic_header hat den Text als codiert, und dein Server decodiert ihn als UTF-8. requests codiert Zugangsdaten, die als Text vorliegen, als Latin-1, die Codierung mit 256 Zeichen, von der früher ausgingen.

Für ASCII-Namen ändert das nichts, weil ASCII-Bytes in beiden gleich sind. Für zoë schon:

Was gibt das aus?

print("zoë".encode("latin-1"), "zoë".encode("utf-8"))

Das strenge UTF-8-Decodieren deines Servers kann das einzelne \xeb von Latin-1 nicht lesen, also bekommt ein Login als zoë trotz richtigem Passwort eine 401. Ein Zeichen, das Latin-1 gar nicht hat, etwa ✓, ist schlimmer: requests löst UnicodeEncodeError aus, bevor irgendetwas gesendet wird.

Die Lösung ist, requests Bytes zu geben, die es unverändert sendet:

credentials = (user.encode("utf-8"), password.encode("utf-8"))
response = session.post(base_url + "/login", auth=credentials, timeout=timeout)

httpx, in Lektion 3, codiert Zugangsdaten, die als Text vorliegen, als UTF-8 und braucht keinen solchen Schritt.

Kein Cache in requests

Eine Session behält Cookies, Header und Verbindungen. Antworten behält sie nicht. Zwei session.get(url)-Aufrufe im Abstand von einer Sekunde senden zwei vollständige Anfragen, auch wenn die erste Antwort cache-control: max-age=60 gesagt hat. Das Revalidieren bleibt deine Aufgabe: Du hältst das Tag fest und sendest es.

response = session.get(url, headers={"if-none-match": etag}, timeout=timeout)

Eine 304 bedeutet, dass deine Kopie noch aktuell ist; eine 200 bringt einen neuen Body und ein neues Tag.

Zwei session.get(url)-Aufrufe im Abstand von einer Sekunde; die erste Antwort hatte cache-control: max-age=60 und ein etag. Was sendet der zweite Aufruf?

Deine Aufgabe

In side_by_side.py:

  1. redirect_trail(response) gibt eine Zeile, f"{r.status_code} {r.url}", für jede Antwort in response.history zurück, die älteste zuerst, und dann eine für response selbst. Ohne Weiterleitung ist das eine Zeile.

  2. revalidate(url, etag, *, session=None, timeout=TIMEOUT) sendet ein GET url mit dem Header if-none-match: etag und timeout=timeout und gibt den zurück. Benutze client = requests if session is None else session, die Zeile aus Python in der Praxis, und lass requests jeder Weiterleitung folgen.

  3. Ändere who_am_i so, dass es seine Zugangsdaten als UTF-8-Bytes übergibt.

Führe dann aus:

python pl_http.py mechanisms

pl_http fügt dem Zustand deines Servers einen Benutzer zoë mit dem Passwort pässwörd ✓ hinzu. Vor deiner Änderung endet der letzte Teil mit der Weigerung von requests:

--- your who_am_i() as zoë ---
raised UnicodeEncodeError: 'latin-1' codec can't encode character '\u2713' in position 9: ordinal not in range(256)

Danach zeigt die Spur die 308 des alten Pfads und das Buch, und das Tag, das du festhältst, entscheidet zwischen 200 und 304:

--- pl_http's session.get() of /v0/books/2?x=1, then your redirect_trail() ---
value: ['308 http://books.test/v0/books/2?x=1', '200 http://books.test/books/2?x=1']
--- requests and replies ---
request 1  GET /books/2  (If-None-Match: "stale")  ->  200 OK  (connection 1)
--- your revalidate() of /books/2 with the tag "stale" ---
value: 200

Der nächste Teil sendet das aktuelle Tag des Buchs und bekommt value: 304, und who_am_i als zoë gibt value: 'zoë' zurück. Klicke auf Check my work. Lektion 3 erledigt dieselben Aufgaben mit httpx, dessen Standardwerte sich in fünf Punkten von denen von requests unterscheiden.

Aufgabe

Lies eine Spur von , revalidiere von Hand und sende Zugangsdaten als .

In side_by_side.py:

  1. redirect_trail(response) gibt eine von zurück: ein f"{r.status_code} {r.url}" für jede Antwort in response.history, die älteste zuerst, dann eines für response selbst.

  2. revalidate(url, etag, *, session=None, timeout=TIMEOUT) sendet ein GET url mit dem Header if-none-match: etag, genau so wie übergeben, und timeout=timeout und gibt den status_code der Antwort zurück. Benutze client = requests if session is None else session. Lass requests Weiterleitungen folgen, wie es das standardmäßig tut: Übergib nicht allow_redirects=False.

  3. who_am_i übergibt seine Zugangsdaten als UTF-8-Bytes: auth=(user.encode("utf-8"), password.encode("utf-8")). Alles andere darin bleibt wie in Lektion 1.

Im fügt python pl_http.py mechanisms deinem Server den Benutzer zoë hinzu und führt dann dein redirect_trail mit einer Anfrage für einen alten /v0-Pfad aus, dein revalidate mit einem veralteten und mit dem aktuellen Tag und dein who_am_i als zoë.

Klicke dann auf Check my work. Es führt deine auf einer echten requests-Session aus, deren Anfragen von einem kleinen Server innerhalb der Prüfung im Speicher beantwortet werden, mit Benutzern, Passwörtern, einer Basis-, einem Buch und Tags, die du noch nicht gesehen hast: Zwei Benutzer, deren Namen oder Passwörter nicht ASCII sind, müssen sich anmelden können; redirect_trail muss eine 308 und das Buch, zu dem sie führt, auflisten und die Reihenfolge einer längeren History beibehalten; revalidate muss über eine 308 hinweg für das aktuelle Tag 304 und für ein veraltetes 200 bekommen und dabei Tag und Timeout bei beiden Hops senden, mit Session und ohne. Nichts aus deinem Terminal wird bewertet.