0%

Hoofdstuk 13 · oefening

Slotproject: een hulpmiddel voor oefenrapporten

Een cache is data met een contract

Dit is het probleem dat je in dit slotproject oplost. Je hebt een commando dat een pagina uit de oefen-API ophaalt en een rapport schrijft. Voer het vijf keer achter elkaar uit en je hebt vijf identieke verzoeken gedaan voor gegevens die niet veranderden. Dat is traag, belast de dienst onnodig en betekent dat je hulpmiddel stopt zodra het netwerk dat doet.

De oplossing is een cache: bewaar het laatste antwoord op schijf en hergebruik het zolang het nog goed genoeg is.

Dat klinkt eenvoudig, en juist bij de eenvoudige versie beginnen de problemen.

Afgeleid betekent niet betrouwbaar

Hoofdstuk 3 leerde je dat afgeleide toestand vervangbaar is. Je kunt .venv verwijderen en opnieuw opbouwen, dus bij verlies gaat niets blijvends verloren. Een cache is op precies dezelfde manier afgeleid: gooi die weg en het programma haalt opnieuw op.

Maar er is een tweede vraag die je bij .venv nooit hoefde te stellen. Wanneer je programma dat cachebestand leest, hoe weet het dan wat het krijgt?

Het bestand is gewone JSON in een map. Er kan van alles mee zijn gebeurd. Het kan half geschreven zijn door een onderbroken uitvoering. Het kan van drie versies geleden zijn, toen je rapport andere velden had. Het kan een cache van pagina 2 zijn terwijl jij pagina 1 vraagt. Het kan met de hand zijn bewerkt door iemand die nieuwsgierig was.

Dat blind hergebruiken is erger dan helemaal geen cache, want een verkeerd antwoord dat direct verschijnt is veel moeilijker op te merken dan geen antwoord.

Dus: afgeleid, ja. Weg te gooien, ja. Zonder meer te vertrouwen, nee. Een cache is invoer en invoer wordt gevalideerd.

Geef de cache een omhulsel

Een losse kopie van het API-antwoord is niet genoeg. Die vertelt wat de gegevens waren, maar niet wat je moet weten voordat je ze hergebruikt. Zet er iets omheen:

{
    "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}
}

Precies vier sleutels, die elk hun plek verdienen door een vraag te beantwoorden die je echt moet stellen:

schema_version beantwoordt “begrijp ik deze structuur nog?” Het is het gehele getal 1. Als je later de cache-indeling verandert, kun je daarmee een oud bestand herkennen en weggooien in plaats van verkeerd lezen.

fetched_at beantwoordt “hoe oud is dit?” Epochseconden, als niet-negatief geheel getal. Zonder die waarde kun je niet bepalen hoe actueel de cache is en is het geheel een gok.

request beantwoordt “is dit wel wat ik vroeg?” Een cache van pagina 2 is een volkomen geldig cachedocument, maar helemaal verkeerd voor een verzoek om pagina 1.

data is de gevalideerde pagina zelf, in de structuur die je validator in hoofdstuk 11 al leerde controleren.

Booleaanse waarden zijn gehele getallen, en dat kan misgaan

Je zag dit in hoofdstuk 11. Het is hier opnieuw belangrijk en verdient daarom herhaling in deze nieuwe context.

In Python is bool een subklasse van int. Daardoor is isinstance(True, int) gelijk aan True. Een controle van tijdstempels met isinstance accepteert dus probleemloos True als geldige fetched_at.

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

type(value) is int wijst True af. Gebruik die controle voor elk veld waarvoor het schema een geheel getal voorschrijft: versie, tijdstempel, pagina, paginagrootte, item-id’s en waarden. JSON heeft een letterlijke waarde true, dus dit is niet hypothetisch.

Valideer voordat je het bestand opent om te schrijven

Eén regel over volgorde, van het soort dat zich pas op het slechtste moment laat merken.

Bouw het volledige document, controleer het en open pas daarna het doelbestand. Niet andersom.

Modus "w" maakt een bestand leeg zodra het wordt geopend. Als je eerst opent en tijdens het schrijven een probleem ontdekt, heb je een prima vorige cache vernietigd en door niets vervangen. Valideer je eerst, dan bereikt een ongeldig document het bestand nooit en blijft de goede versie van gisteren intact.

Je zag hetzelfde principe in hoofdstuk 9 bij de rapportschrijver. Het geldt algemener: doe het werk dat kan mislukken vóór de stap die iets vernietigt.

Voeg de strengere itemregels van het rapport toe

Het transportcontract uit hoofdstuk 11 staat elk geheel getal toe als item-id of waarde en elke niet-lege naam of categorie. Dit rapportagehulpmiddel kiest een strenger functioneel contract: id’s beginnen bij 1, waarden zijn niet-negatief en namen en categorieën bevatten geen witruimte aan de randen.

Voeg _validate_report_page(document) toe. Roep eerst api_catalog.validate_items_page(document) aan, controleer daarna die drie rapportregels en geef hetzelfde object terug. Door deze extra laag apart te houden, is duidelijk welke regels uit de servicestructuur komen en welke het rapport zelf koos.

Nu jij

Implementeer vier functies in practice_report.py:

  • validate_cache_document(document, *, page, page_size) controleert het omhulsel en de pagina erin, bevestigt dat het opgeslagen verzoek overeenkomt met wat werd gevraagd en geeft de gegevens en hun tijdstempel terug. Alles wat niet klopt werpt ValueError op.

  • read_cache(path, *, page, page_size) opent het bestand, ontleedt de JSON en geeft die door aan de validator.

  • make_cache_document(page_document, *, fetched_at) bouwt het omhulsel met vier sleutels om een gevalideerde pagina.

  • write_cache(page_document, path, *, fetched_at) bouwt het document, valideert het en schrijft het pas daarna als UTF-8-JSON met inspringing van twee spaties en één afsluitend regeleinde.

Gebruik _validate_report_page voor de pagina in data. De validator uit hoofdstuk 11 hier kopiëren zou je twee versies geven die je gelijk moet houden.

Waarom valideer je een cachebestand dat je eigen programma schreef?

Waarom controleer je type(value) is int in plaats van isinstance(value, int) voor fetched_at?

Opdracht

Implementeer de extra validatielaag voor rapportpagina’s en de exacte cachevalidator, lezer, constructor en canonieke schrijver in practice_report.py. Behoud api_catalog.py en de herkenbare begininhoud van gegenereerde bestanden.