Abschlussprojekt: Ein Werkzeug für Übungsberichte · Übung
Netzwerk, Cache oder Offline-Daten auswählen
Du hast jetzt drei mögliche Quellen für eine Seite: das Netzwerk, den Cache auf dem Datenträger und eine selbst verfasste Datei, die keines von beiden verwendet. Diese Lektion entscheidet, welche Quelle ein bestimmter Lauf nutzt.
Diese Entscheidung ist das Herz des Werkzeugs. Hier gehen solche Programme meist schief. Nicht, weil eine einzelne Regel schwierig wäre, sondern weil die Regeln einzeln entdeckt und nachträglich angebaut werden, bis niemand ohne Ausführung sagen kann, was das Programm tun wird.
Schreibe deshalb zuerst die Tabelle.
Die Tabelle
| Situation | Abruf? | Rückgriff auf Cache? | Quelle |
|---|---|---|---|
--offline-source angegeben | nein | niemals | offline |
| Cache gültig und aktuell | nein | nicht nötig | fresh-cache |
| Cache fehlt | einmal | nein | network |
| Cache ungültig | einmal | nein, mit Warnung | network |
| Cache gültig, aber veraltet | einmal | ja, bei erwartetem Fehler | network oder stale-cache |
--refresh angegeben | einmal | niemals | network |
Sechs Zeilen. Jeder Lauf landet in genau einer davon. Lies sie zweimal, denn alles Weitere beschreibt nur diese Zeilen genauer.
Offline heißt offline
--offline-source liest die selbst verfasste Seite und hört dort auf. Es prüft weder den Cache noch liest es die Uhr oder sendet eine Anfrage. Wenn die Datei fehlt oder nicht zur angeforderten Seite passt, ist das ein Fehler, kein Grund für einen Abruf.
Das muss der allererste Zweig sein. Alles, was davor läuft, einschließlich des Lesens der Uhr, verletzt die Zusage, dass der Offline-Modus nichts anderes berührt.
Fehlend und ungültig sind unterschiedliche Fehlerfälle
Beide führen zum Abruf, sind aber nicht dasselbe Ereignis und sollten nicht gleich aussehen.
Ein fehlender Cache ist völlig normal. Es ist der erste Lauf. Oder jemand hat aufgeräumt. Sage nichts und rufe ab.
Ein ungültiger Cache verdient eine Meldung. Die Datei war vorhanden und konnte nicht verwendet werden. Wer nie davon erfährt, wird sich fragen, warum das Werkzeug langsam ist. Gib eine Warnung aus und rufe dann ab:
Warning: ignored invalid cache practice-cache.json.
Eine Zeile auf stderr, dann geht es weiter. Das ist eine Warnung statt eines Fehlers, weil das Programm sich erholt hat.
Achte darauf, welche Ausnahmen zu welchem Fall gehören. FileNotFoundError bedeutet fehlend. json.JSONDecodeError und ValueError bedeuten ungültig. Jeder andere OSError, etwa ein Berechtigungsproblem, ist keines von beiden: Dein Programm konnte eine angeforderte Datei nicht lesen. So zu tun, als wäre das ein leerer Cache, würde ein echtes Problem verstecken.
Veraltet ist ein Ersatz, keine erste Wahl
Ein veralteter Cache ist alt, aber strukturell in Ordnung. Der Plan ist, aktuelle Daten abzurufen. Der Wert der alten Kopie liegt darin, dass sie da ist, wenn der Abruf scheitert.
Versuche also den Abruf. Bei Erfolg verwendest du die neue Seite. Scheitert er auf eine der erwartbaren Arten einer Netzwerkanfrage, greifst du auf die alte Kopie zurück und meldest dies mit einer Warnung:
Warning: refresh failed; using stale cache practice-cache.json.
„Erwartbar“ hat hier eine genaue Bedeutung: die Fehler aus Kapitel 11, also requests.Timeout, requests.ConnectionError, requests.HTTPError, ein JSON-Dekodierfehler oder ein ValueError bei der Antwortvalidierung.
Es bedeutet nicht einen TypeError durch einen Fehler in deinem eigenen Code. Wenn deine Abruffunktion einen Fehler hat und du ihn hier abfängst, liefert das Programm still alte Daten und meldet Erfolg. Der Fehler gelangt getarnt in den produktiven Einsatz. Lass das Programm abbrechen.
Refresh heißt neu abrufen
--refresh sagt: Ich will aktuelle Daten und weiß, dass ich sie möglicherweise nicht bekomme. Es ruft einmal ab und greift niemals auf den Cache zurück, selbst wenn eine vollkommen brauchbare alte Kopie bereitliegt.
Jemandem zwischengespeicherte Daten zu liefern, der ausdrücklich aktuelle angefordert hat, wäre das Verwirrendste, was dieses Werkzeug tun könnte.
Niemals mehr als eine Anfrage
Jede Zeile dieser Tabelle sendet null oder eine GET-Anfrage. Niemals zwei.
Keine Wiederholungsschleife, keine Schleife über mehrere Seiten, kein „versuche es noch einmal mit kleinerer Seitengröße“. Wenn eine Anfrage scheitert, meldet dieses Programm den Fehler oder nutzt den Ersatz. Eine Wiederholungsstrategie ist eine eigenständige Entwurfsentscheidung, die zunehmende Wartezeiten, eine Obergrenze und einen Grund braucht. Nichts davon gehört mitten in eine Funktion zur Quellenauswahl.
Jetzt bist du dran
Implementiere diese Schnittstelle in practice_report.py:
import time
DEFAULT_TIMEOUT = api_catalog.DEFAULT_TIMEOUT
def choose_page(
page, page_size, cache_path, max_age, *,
refresh=False, offline_source=None, timeout=DEFAULT_TIMEOUT,
session=None, now_epoch=None,
):
...
Gib ein Tupel mit vier Elementen zurück: (page_document, source, fetched_at, warnings). source ist genau "offline", "fresh-cache", "network" oder "stale-cache". Verwende None als Offline-Zeitstempel, den gespeicherten Cache-Zeitstempel für beide Cache-Quellen und den einen Wert now_epoch für einen erfolgreichen Netzwerkabruf. warnings ist eine Liste der oben gezeigten Warnstrings ohne abschließende Zeilenumbrüche. Gib die Liste zurück; die CLI gibt sie später aus.
offline_source bezeichnet eine UTF-8-JSON-Datei mit einer bloßen Seite, keiner Cache-Hülle. Parse sie, validiere sie mit _validate_report_page und verlange, dass ihre Seite und Seitengröße den angeforderten ganzen Zahlen entsprechen. Eine Hilfsfunktion wie read_offline_page(path, *, page, page_size) kann diesen Zweig kurz halten.
Verlange in den anderen Modi echte ganze Zahlen für Seite (1 bis 10) und Seitengröße (1 bis 3) sowie nicht negative max_age und now_epoch. Rufe über api_catalog.fetch_items_page(page, page_size, timeout=timeout, session=session) ab und wende _validate_report_page auf das Ergebnis an. Lass unabhängige Programmierfehler bei Anfragen ebenso wie TypeError nach außen gelangen.
Diese Funktion wählt Daten aus; sie schreibt weder Cache noch Bericht. Überlasse diese Schreibvorgänge der CLI-Lektion.
Behandle die Zweige in der Reihenfolge der Tabelle. Erfasse int(time.time()) einmal, nur wenn kein now_epoch übergeben wurde, und erst nachdem der Offline-Zweig ausgeschlossen wurde.
Teste choose_page direkt mit einer Fake-Session und einem ausdrücklichen now_epoch. Der Kommandozeileneinstieg wird erst in Lektion 5 fertiggestellt. practice_report.py --refresh auszuführen ist also noch kein Test dieser Funktion.
Eine Cache-Datei ist vorhanden, enthält aber fehlerhaftes JSON. Was sollte passieren?
Warum lehnt --refresh den Rückgriff auf einen veralteten Cache ab?
Warum darf ein im Abruf ausgelöster TypeError keinen Rückgriff auf den alten Cache auslösen?
Aufgabe
Implementiere genau die Entscheidungstabelle für Netzwerk, Cache und Offline-Daten mit einem übergebenen Zeitpunkt, höchstens einem zeitlich begrenzten Abruf und eng begrenzten Ausnahmen für den Rückgriff auf alte Daten.