0%

Abschlussprojekt: der Leselisten-Dienst und sein Client · Abschlussprojekt

Abschlussprojekt: Den Client fertigstellen

Dein Server beantwortet ein passendes If-None-Match jetzt mit einer 304 ohne Body und gibt jedem neuen Buch eine location. Der Client benutzt keins von beiden: Jedes show lädt das ganze Buch herunter, und add sagt nie, wo das neue Buch wohnt. Diese Lektion baut beide Client-Hälften.

Die Cache-Regeln sind die, die du in Kapitel 8, Lektion 5, für cached_get in wire.py geschrieben hast, das seine Einträge in einem aufbewahrt hat, das der Aufrufer übergeben hat. Dieses Dictionary hat so lange gelebt wie das Programm. Jeder Befehl von reading_client.py ist ein neuer Python-Lauf, also muss ein Cache, der dem zweiten show helfen soll, das erste überdauern: Er lebt in einer Datei, reading_cache.json, in dem Ordner, in dem der Client läuft.

Eine einzige Uhrablesung

Frische braucht die aktuelle Uhrzeit. main liest jetzt die Uhr, einmal:

def main(argv=None, *, session=None, now=None, cache_path=CACHE_FILE):
    ...
    if now is None:
        now = time.time()

und reicht now nach innen an run weiter, das es an cached_show weitergibt. Das ist die eingeschleuste Zeitgrenze aus dem Abschlussprojekt von Python in der Praxis, Kapitel 13, Lektion 2. Nichts unterhalb von main ruft time.time() auf, also übergibt ein Test now=0 und dann now=59 und bekommt bei jedem Lauf dieselbe Antwort. In den Kapiteln 7 bis 9 hat kein Code von dir eine Uhr gelesen; ein Programm mit braucht eine Stelle, die es tut, und main ist diese Stelle.

cache_path funktioniert genauso: Es ist standardmäßig reading_cache.json, und jeder Test, der mit dem Projekt gekommen ist, übergibt stattdessen einen Pfad innerhalb von tmp_path.

Warum liest main die Uhr und übergibt now, statt dass cached_show selbst time.time() aufruft?

Die Regeln, die du schon geschrieben hast

SERVICE_CONTRACT.md nennt sie unter “The client” und dann “The cache” für diese Datei. Es sind die aus Kapitel 8:

  • Die Datei enthält ein -. Seine Schlüssel sind die angefragten , und jeder ist {"etag": ..., "expires": ..., "book": ...}.

  • Ein Eintrag ist frisch, solange now kleiner als sein expires ist. Ein frischer Eintrag wird ohne Anfrage angezeigt.

  • Ein veralteter Eintrag wird mit If-None-Match und seinem etag erneut angefragt. Eine 304 behält das Buch und setzt expires auf now plus das eigene max-age der 304. Eine 200 ersetzt den Eintrag.

  • Nur eine 200 mit einem etag wird gespeichert, mit expires auf now plus ihr max-age gesetzt, oder auf 0, wenn es keins gibt. Jede andere Antwort entfernt den Eintrag für diese URL.

Python in der Praxis hat das Lesen und Schreiben einer solchen Datei behandelt, also kommen beide mit dem Projekt: load_cache(cache_path) gibt das gespeicherte Dictionary zurück, oder {}, wenn die Datei fehlt, nicht gelesen werden kann, kein JSON ist oder kein JSON-Objekt ist, und save_cache(cache_path, cache) schreibt das Dictionary als -JSON zurück.

Der Schlüssel des Caches ist die URL, nach der der Client gefragt hat, also behält ein show über die alte /v0-Basis eigene Einträge, obwohl der Server es weiterleitet.

Eine Location ist noch keine URL

Die 201 deines Servers enthält location: /books/3. Das ist ein Pfad, relativ zu der URL, nach der der Client gefragt hat, und ein Client, der ihn so ausgibt, wie er kam, gibt eine halbe Adresse aus. Kapitel 6, Lektion 2 hat solche Werte mit urljoin aus urllib.parse aufgelöst, und reading_client.py importiert es schon:

urljoin("http://127.0.0.1:8080/books", "/books/3")    # 'http://127.0.0.1:8080/books/3'

Löse ihn gegen response.url auf, wo requests am Ende gelandet ist, nicht gegen --base-url: Nach einer unterscheiden sich die beiden, und die Antwort, die die location enthielt, ist die richtige Basis.

Meilensteine

Zuerst max_age(response). Teile response.headers.get("cache-control", "") an den Kommas. Für das Element, das ohne Leerzeichen und kleingeschrieben mit max-age= beginnt, gib die ASCII-Ziffern danach als int zurück. Gib 0 zurück, wenn es keins gibt. Die headers von requests unterscheiden nicht zwischen Groß- und Kleinschreibung, also findet "cache-control" auch Cache-Control.

Dann cached_show(base_url, book_id, *, cache_path, now, session=None, timeout=TIMEOUT). Baue die URL mit book_url, lade den Cache und schlage den Eintrag nach. Gib ein Dictionary mit genau vier Schlüsseln zurück:

SituationAnfrage"status""book""response""source"
ein frischer Eintragkeine200das gespeicherte BuchNone"cache"
ein veralteter Eintrag und eine 304GET mit If-None-Match200das gespeicherte Buchdie 304"revalidated"
alles andereGET, mit If-None-Match, falls gespeichertder Status der Antwortihr JSON bei einer 200, sonst Nonedie Antwort"network"

Sende die Anfrage über die Session, wie call es tut, client = requests if session is None else session, mit timeout=timeout, und speichere die Datei, wann immer du einen Eintrag änderst. Etwa zwanzig Zeilen.

Dann run. Sein show-Zweig ruft statt call cached_show(args.base_url, args.book_id, cache_path=cache_path, now=now, session=session) auf. Ein Status von 400 oder darüber gibt describe_error(result["response"]) auf der Standardfehlerausgabe aus und gibt 2 zurück; sonst gibt er book_line(result["book"]) aus und gibt 0 zurück.

Zuletzt die add-Zeile. Nach added die Zeile des Buchs, dann at und urljoin(response.url, response.headers["location"]).

python -m pytest -q

Die Tests, die mit dem Projekt gekommen sind, sollten weiterhin bestehen.

Die Exit-Status gelten weiterhin, und die Shell bewahrt den letzten in $? auf. Ein show ohne Buch-ID hält vor jeder Anfrage an, braucht also keinen Server:

python reading_client.py show --base-url http://books.test
echo $?

argparse gibt auf der Standardfehlerausgabe einen Verwendungsblock aus, der mit reading_client.py: error: show needs a book id endet, und echo $? gibt 2 aus. Eine 404 endet ebenfalls mit 2; keine Antwort endet mit 1.

Deine Aufgabe

  1. Schreibe max_age und cached_show.

  2. Schicke show in run durch cached_show.

  3. Gib nach added at und die absolute URL aus.

  4. Führe python -m pytest -q aus und klicke dann auf Check my work.

Eine fehlschlagende Prüfung nennt den Befehl, sein now und die Regel. Bitte Monty, die Meldung mit “The cache” in SERVICE_CONTRACT.md zu vergleichen, ohne die Korrektur zu schreiben.

Der Client merkt sich jetzt, was er abgerufen hat, und sagt, wo ein neues Buch wohnt. Weiterleitungen werden immer noch stillschweigend gefolgt; das ändert die nächste Lektion, bevor sie beide Hälften zusammen laufen lässt.

Aufgabe

Stelle zwei der Client-Zeilen von SERVICE_CONTRACT.md fertig: den Cache, den show führt, und die absolute nach add.

  1. max_age(response): die Zahl nach max-age= im cache-control der Antwort, als int, oder 0, wenn es keine gibt.

  2. cached_show(base_url, book_id, *, cache_path, now, session=None, timeout=TIMEOUT), nach “The cache” im Vertrag: Ein frischer Eintrag (now kleiner als sein expires) wird ohne Anfrage zurückgegeben; ein veralteter wird mit If-None-Match und seinem etag angefragt; eine 304 behält das Buch und setzt expires auf now plus das eigene max-age der 304; eine 200 mit einem etag wird unter der angefragten URL als {"etag", "expires", "book"} gespeichert; jede andere Antwort entfernt den Eintrag dieser URL. Benutze load_cache und save_cache, die Session, die du bekommst, und timeout. Gib {"status", "book", "response", "source"} zurück, wie die Tabelle der Lektion zeigt.

  3. In run geht show mit now und cache_path durch cached_show; ein Fehlerstatus gibt describe_error auf der Standardfehlerausgabe aus und gibt 2 zurück.

  4. add gibt added ID: TITLE by AUTHOR at URL aus, wobei URL urljoin(response.url, response.headers["location"]) ist.

Behalte jede andere Client-Zeile so bei, wie sie ist: einen timeout bei jeder Anfrage, authorization: Bearer TOKEN nur mit --token, die Exit-Status 0, 1 und 2 mit einer Zeile auf der Standardfehlerausgabe. Führe im python -m pytest -q aus.

Klicke dann auf Check my work. Es führt reading_client.main mit einer Session aus, die jede Anfrage aufzeichnet und nach einem Skript antwortet, mit einer Cache-Datei in einem Ordner, den es anlegt, und prüft: den Timeout bei jedem Befehl und dass call und cached_show einen Timeout weitergeben, den sie bekommen; Exit-Status und Ausgabeströme bei Antworten 404, 409, 500, 503 und 401, einem Timeout beim Verbindungsaufbau, einem Timeout beim Lesen, einer abgelehnten Verbindung und drei Verwendungsfehlern; , URL, Token und -Body jedes Schreibzugriffs; max_age mit fünf cache-control-; load_cache bei einer fehlenden Datei, einem Ordner, kaputtem JSON, einer und Bytes, die kein sind; show zu den Zeiten 1000 bis 1099 mit max-age-Werten von 45 und 7, über einen ersten Abruf, einen frischen Eintrag, eine 304, eine ersetzende 200, eine 404 und eine 200 ohne etag; und die absolute URL nach add für vier Basis-URLs und location-Werte. Während dein Client läuft, kann reading_client.requests nichts senden, also schlägt eine Anfrage, die die Session umgeht, mit einer Meldung fehl. Nichts öffnet einen , und nichts aus deinem Terminal wird bewertet.