Hoofdstuk 11 · oefening
Met een API communiceren
Doe één gecontroleerd verzoek
Alle invoer die je catalogus tot nu toe las, kwam uit een bestand ernaast. Bestandsbewerkingen kunnen ook mislukken, zoals hoofdstuk 9 liet zien. Een netwerkdienst voegt nog een grens toe: je verzoek en het antwoord reizen tussen programma’s en beide kanten kunnen ophouden met reageren.
Dit hoofdstuk maakt die grenzen rond verzoeken en antwoorden expliciet, zodat je code bruikbare gegevens kan onderscheiden van een mislukte uitwisseling.
De dienst hier is een oefen-API binnen Python Land, op een geïsoleerd netwerk dat je code kan bereiken maar het openbare internet niet. De dienst is bewust saai en voorspelbaar, ook in de manieren waarop die faalt. Daardoor kun je er goed mee leren.
Richt de omgeving en het recept in
Dezelfde scheiding als altijd: een tijdelijke omgeving en een blijvend bestand waarmee je die opnieuw kunt opbouwen.
python -m venv .venv
.venv/bin/python -m pip install requests==2.34.2
nano requirements.txt
.venv/bin/python -c "import requests; print(requests.__version__)"
Zet precies requests==2.34.2 in requirements.txt, met een regeleinde aan het eind.
Dat laatste commando is een handig trucje: python -c voert een kort programma uit dat je rechtstreeks op de commandoregel opgeeft. De aanhalingstekens houden het bij elkaar als één argument. Zo hoef je voor een controle van één regel geen bestand te schrijven.
Eén volledig uitgeschreven verzoek
Open api_catalog.py. Dit is de functie die je bouwt:
def request_json(path, *, params=None, timeout=DEFAULT_TIMEOUT, session=None):
client = requests if session is None else session
response = client.get(
API_BASE + path,
params=params,
headers=PRACTICE_HEADERS,
timeout=timeout,
)
return response.json()
Er gebeuren vier dingen, en elk is een keuze.
De losse * maakt elke parameter erna keyword-only. Een aanroeper moet timeout=(1.0, 1.0) schrijven in plaats van een losse tuple op de derde positie door te geven. Met vier argumenten die op elkaar lijken, is die verplichte naam de extra toetsaanslagen waard.
De header gaat met elk verzoek mee. X-Practice-Name: python-in-practice identificeert deze cursus bij de dienst. Het is geen wachtwoord en er is niets geheim in dit project.
De time-out is niet optioneel. Die heeft een standaardwaarde, maar die is expliciet en wordt altijd meegestuurd. Een verzoek zonder time-out kan eindeloos wachten. Les 4 gaat volledig over deze grens.
De parameter session is de interessante, dus die krijgt een eigen onderdeel.
Waarom je een client aan de functie kunt meegeven
Kijk nog eens naar de eerste regel:
client = requests if session is None else session
Normaal is session None en gebruikt de functie rechtstreeks requests. Dat gebeurt wanneer het programma echt draait.
Maar een aanroeper kan iets anders meegeven dat een methode .get() heeft. De functie gebruikt dat dan. In een test geef je een klein namaakobject mee dat vastlegt wat er werd gevraagd en teruggeeft wat jij bepaalt. Er komt geen netwerk aan te pas. Niets wacht. Het resultaat is elke keer hetzelfde.
Sta hier even bij stil, want dit is een belangrijk idee dat je makkelijk voor een trucje kunt aanzien. Je bouwt geen tweede manier om verzoeken te doen. Je maakt het ene punt waar je code de buitenwereld raakt vervangbaar, zodat je die buitenwereld kunt vervangen wanneer je wilt onderzoeken wat je code deed. Elke test die je in dit hoofdstuk en het slotproject schrijft, steunt op dat vervangbare punt.
Zo’n vervanger hoeft niet uitgebreid te zijn. Die heeft een .get() nodig die vastlegt wat er werd meegegeven en iets teruggeeft met een .json(). types.SimpleNamespace uit de standaardbibliotheek is genoeg om er een te maken:
import types
def fake_session(document, calls):
def decode_json():
return document
def get(url, **kwargs):
calls.append((url, kwargs))
return types.SimpleNamespace(json=decode_json)
return types.SimpleNamespace(get=get)
SimpleNamespace bouwt een object uit de keywordargumenten die je meegeeft. Zo levert types.SimpleNamespace(get=get) iets op met een attribuut .get en verder niets. Dat is alles wat request_json ooit vraagt.
Probeer het:
calls = []
result = request_json("/items", session=fake_session({"items": []}, calls))
print(calls[0])
calls[0] toont de exacte URL, parameters, headers en time-out die je functie koos, zonder dat er één byte over een netwerk ging.
Probeer ook het echte verzoek
De interne dienst is beschikbaar als je een live antwoord wilt zien:
.venv/bin/python -c "from api_catalog import request_json; print(request_json('/items'))"
Op dit moment decodeert dit de antwoordinhoud ongeacht wat de server heeft gemeld. Als de dienst met een fout antwoordt, geeft deze functie die probleemloos terug alsof het gegevens zijn. Dat is een echt gat, dat les 3 dicht.
Waarom accepteert request_json een optionele session?
Als het idee van een namaaksessie nog niet duidelijk is, vraag Monty dan stap voor stap te laten zien waar client naar verwijst bij een gewone aanroep en bij een testaanroep. Dit is het idee in dit hoofdstuk waarvoor je rustig de tijd mag nemen.
Opdracht
Leg precies requests==2.34.2 vast in requirements.txt. Implementeer request_json als één GET naar de vaste interne basis. Gebruik de meegegeven session wanneer die beschikbaar is en geef altijd params, de vaste oefenheader en een expliciete time-out door. Geef voorlopig de gedecodeerde JSON terug.