0%

Kapitel 13 · Übung

Abschlussprojekt: Ein Werkzeug für Übungsberichte

Ein Cache braucht klare Datenvorgaben

Dieses Abschlussprojekt löst folgendes Problem: Du hast einen Befehl, der eine Seite von der Übungs-API abruft und einen Bericht schreibt. Wenn du ihn fünfmal hintereinander ausführst, stellst du fünf identische Anfragen für unveränderte Daten. Das ist langsam, belastet den Dienst unnötig und bedeutet, dass dein Werkzeug nicht mehr funktioniert, sobald das Netzwerk ausfällt.

Die Lösung ist ein Cache: Speichere die letzte Antwort auf dem Datenträger und verwende sie erneut, solange sie noch gut genug ist.

Das klingt einfach. Und bei der einfachen Variante beginnen die Schwierigkeiten.

Abgeleitet bedeutet nicht vertrauenswürdig

In Kapitel 3 hast du gelernt, dass abgeleiteter Zustand ersetzbar ist. .venv lässt sich löschen und neu aufbauen; sie zu verlieren kostet also nichts. Ein Cache ist im selben Sinn abgeleitet: Wirf ihn weg, und das Programm ruft erneut ab.

Aber hier stellt sich eine zweite Frage, die du bei .venv nie stellen musstest. Wenn dein Programm die Cache-Datei liest, woher weiß es, was es bekommt?

Die Datei ist gewöhnliches JSON in einem Ordner. Mit ihr kann alles Mögliche passiert sein. Sie könnte nach einem unterbrochenen Lauf nur halb geschrieben sein. Sie könnte von vor drei Versionen stammen, als dein Bericht andere Felder hatte. Sie könnte Seite 2 zwischenspeichern, während du Seite 1 anforderst. Jemand könnte sie aus Neugier von Hand bearbeitet haben.

Sie blind wiederzuverwenden ist schlimmer, als gar keinen Cache zu haben. Eine falsche Antwort, die sofort ankommt, ist viel schwerer zu bemerken als eine fehlende Antwort.

Also: abgeleitet, ja. Entbehrlich, ja. Auf den ersten Blick vertrauenswürdig, nein. Ein Cache ist Eingabe, und Eingaben werden validiert.

Gib dem Cache eine Hülle

Eine bloße Kopie der API-Antwort reicht nicht. Sie sagt dir, welche Daten vorlagen, aber nicht alles, was du vor ihrer Wiederverwendung wissen musst. Verpacke sie:

{
    "schema_version": 1,
    "fetched_at": 1754000000,
    "request": {"page": 1, "page_size": 3},
    "data": {"items": [], "page": 1, "page_size": 3, "total": 0, "has_next": false, "next_page": null}
}

Genau vier Schlüssel, und jeder hat seinen Platz, weil er eine Frage beantwortet, die du tatsächlich stellen musst:

schema_version beantwortet: „Verstehe ich diese Struktur noch?“ Der Wert ist die ganze Zahl 1. Wenn du später das Cache-Format änderst, lässt sich damit eine alte Datei erkennen und verwerfen, statt sie falsch zu lesen.

fetched_at beantwortet: „Wie alt ist das?“ Sekunden seit der Unix-Epoche, als nicht negative ganze Zahl. Ohne diesen Wert lässt sich die Aktualität nicht beurteilen, und der ganze Cache ist ein Ratespiel.

request beantwortet: „Ist das überhaupt das, wonach ich gefragt habe?“ Ein Cache von Seite 2 ist ein vollkommen gültiges Cache-Dokument und für eine Anfrage nach Seite 1 völlig falsch.

data ist die validierte Seite selbst, in der Struktur, die dein Validator aus Kapitel 11 bereits prüfen kann.

Wahrheitswerte sind ganze Zahlen, und das kann schiefgehen

Das ist dir in Kapitel 11 begegnet und wird hier wieder wichtig. Es lohnt sich deshalb, es im neuen Zusammenhang zu wiederholen.

In Python ist bool eine Unterklasse von int. Deshalb ist isinstance(True, int) gleich True. Eine Zeitstempelprüfung mit isinstance würde also bereitwillig True als gültiges fetched_at akzeptieren.

if type(value) is not int or value < 0:
    raise ValueError("fetched_at must be a non-negative integer")

type(value) is int weist True zurück. Verwende es für jedes Feld, für das das Schema eine ganze Zahl verlangt: Version, Zeitstempel, Seite, Seitengröße, Artikel-IDs und Werte. JSON hat ein Literal true; das ist also kein rein theoretischer Fall.

Validiere, bevor du die Datei zum Schreiben öffnest

Hier gilt eine Regel zur Reihenfolge, die sich sonst erst im ungünstigsten Moment bemerkbar macht.

Baue das vollständige Dokument auf, prüfe es und öffne erst dann das Ziel. Nicht umgekehrt.

Modus "w" leert eine Datei in dem Moment, in dem sie geöffnet wird. Wenn du zuerst öffnest und beim Schreiben ein Problem entdeckst, hast du einen vollkommen brauchbaren bisherigen Cache zerstört und durch nichts ersetzt. Bei vorheriger Validierung erreicht ein schlechtes Dokument die Datei gar nicht, und der gute Stand von gestern bleibt erhalten.

Dasselbe Prinzip hast du in Kapitel 9 beim Berichtsschreiber gesehen. Es lässt sich verallgemeinern: Erledige die Arbeit, die fehlschlagen kann, vor dem Schritt, der etwas zerstört.

Ergänze die strengeren Artikelregeln des Berichts

Die Transportvorgaben aus Kapitel 11 erlauben beliebige ganze Zahlen als Artikel-ID oder Wert sowie beliebige nicht leere Namen oder Kategorien. Dieses Berichtswerkzeug wählt engere fachliche Vorgaben: IDs beginnen bei 1, Werte sind nicht negativ, und Namen sowie Kategorien haben keinen umgebenden Leerraum.

Ergänze _validate_report_page(document). Rufe zuerst api_catalog.validate_items_page(document) auf, prüfe dann diese drei berichtsspezifischen Regeln und gib dasselbe Objekt zurück. Die getrennte Hüllfunktion macht deutlich, welche Regeln aus der Dienststruktur stammen und welche dieser Bericht festgelegt hat.

Jetzt bist du dran

Implementiere vier Funktionen in practice_report.py:

  • validate_cache_document(document, *, page, page_size) prüft die Hülle und die darin enthaltene Seite, bestätigt die Übereinstimmung mit der angeforderten Seite und gibt die Daten samt Zeitstempel zurück. Jeder Verstoß löst ValueError aus.

  • read_cache(path, *, page, page_size) öffnet die Datei, parst das JSON und übergibt es dem Validator.

  • make_cache_document(page_document, *, fetched_at) baut die Hülle mit vier Schlüsseln um eine validierte Seite.

  • write_cache(page_document, path, *, fetched_at) baut das Dokument auf, validiert es und schreibt es erst danach als UTF-8-JSON mit zwei Leerzeichen Einrückung und einem abschließenden Zeilenumbruch.

Verwende _validate_report_page für die Seite in data. Den Validator aus Kapitel 11 hierher zu kopieren würde zwei Versionen schaffen, die du im Einklang halten müsstest.

Warum validierst du eine Cache-Datei, die dein eigenes Programm geschrieben hat?

Warum prüfst du für fetched_at mit type(value) is int statt mit isinstance(value, int)?

Aufgabe

Implementiere in practice_report.py die berichtsspezifische Seiten-Hüllfunktion sowie den genau vorgegebenen Cache-Validator, Leser, Konstruktor und Schreiber im festgelegten Format. Erhalte api_catalog.py und die Kontrollinhalte in erzeugten Dateien.