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 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 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
nowkleiner als seinexpiresist. Ein frischer Eintrag wird ohne Anfrage angezeigt.Ein veralteter Eintrag wird mit
If-None-Matchund seinemetagerneut angefragt. Eine 304 behält das Buch und setztexpiresaufnowplus das eigenemax-ageder 304. Eine 200 ersetzt den Eintrag.Nur eine 200 mit einem
etagwird gespeichert, mitexpiresaufnowplus ihrmax-agegesetzt, 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
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 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:
| Situation | Anfrage | "status" | "book" | "response" | "source" |
|---|---|---|---|---|---|
| ein frischer Eintrag | keine | 200 | das gespeicherte Buch | None | "cache" |
| ein veralteter Eintrag und eine 304 | GET mit If-None-Match | 200 | das gespeicherte Buch | die 304 | "revalidated" |
| alles andere | GET, mit If-None-Match, falls gespeichert | der Status der Antwort | ihr JSON bei einer 200, sonst None | die 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
Schreibe
max_ageundcached_show.Schicke
showinrundurchcached_show.Gib nach
addedatund die absolute URL aus.Führe
python -m pytest -qaus 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 add.
max_age(response): die Zahl nachmax-age=imcache-controlder Antwort, als int, oder 0, wenn es keine gibt.cached_show(base_url, book_id, *, cache_path, now, session=None, timeout=TIMEOUT), nach “The cache” im Vertrag: Ein frischer Eintrag (nowkleiner als seinexpires) wird ohne Anfrage zurückgegeben; ein veralteter wird mitIf-None-Matchund seinemetagangefragt; eine 304 behält das Buch und setztexpiresaufnowplus das eigenemax-ageder 304; eine 200 mit einemetagwird unter der angefragten URL als{"etag", "expires", "book"}gespeichert; jede andere Antwort entfernt den Eintrag dieser URL. Benutzeload_cacheundsave_cache, die Session, die du bekommst, undtimeout. Gib{"status", "book", "response", "source"}zurück, wie die Tabelle der Lektion zeigt.In
rungehtshowmitnowundcache_pathdurchcached_show; ein Fehlerstatus gibtdescribe_errorauf der Standardfehlerausgabe aus und gibt 2 zurück.addgibtadded ID: TITLE by AUTHOR at URLaus, wobei URLurljoin(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; max_age mit fünf cache-control-load_cache bei einer fehlenden Datei, einem Ordner, kaputtem JSON, einer 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