0%

Kapitel 11 · Übung

Mit einer API kommunizieren

Eine kontrollierte Anfrage senden

Alle Eingaben, die dein Katalog bisher gelesen hat, kamen aus einer Datei direkt neben ihm. Auch Dateioperationen können fehlschlagen, wie Kapitel 9 gezeigt hat. Ein Netzwerkdienst bringt eine weitere Grenze hinzu: Deine Anfrage und ihre Antwort wandern zwischen Programmen, und jede Seite kann aufhören zu antworten.

Dieses Kapitel macht diese Grenzen von Anfrage und Antwort ausdrücklich sichtbar, damit dein Code brauchbare Daten von einem fehlgeschlagenen Austausch unterscheiden kann.

Der Dienst hier ist eine Übungs-API innerhalb von Python Land. Sie liegt in einem isolierten Netzwerk, das dein Code erreichen kann, das öffentliche Internet aber nicht. Sie ist bewusst unspektakulär und vorhersehbar, auch in ihren Fehlerfällen. Genau das macht sie zum Lernen nützlich.

Richte die Umgebung und ihre Anleitung ein

Die Trennung bleibt dieselbe: eine vorübergehende Umgebung und eine dauerhafte Datei, mit der sie sich wiederherstellen lässt.

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__)"

Schreibe genau requests==2.34.2 in requirements.txt, mit einem Zeilenumbruch am Ende.

Der letzte Befehl ist ein kleiner, nützlicher Kniff: python -c führt ein kurzes Programm aus, das du direkt auf der Kommandozeile angibst. Die Anführungszeichen halten es als ein Argument zusammen. So brauchst du für eine Prüfung mit einer Zeile keine Datei zu schreiben.

Eine Anfrage, vollständig ausgeschrieben

Öffne api_catalog.py. Diese Funktion baust du:

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()

Hier passieren vier Dinge, und jedes davon ist eine Entscheidung.

Das alleinstehende * macht alle folgenden Parameter zu reinen Schlüsselwortparametern. Beim Aufruf musst du timeout=(1.0, 1.0) schreiben, statt ein bloßes Tupel an dritter Position zu übergeben. Bei vier ähnlich aussehenden Argumenten lohnt sich die zusätzliche Schreibarbeit für eindeutige Namen.

Der Header gehört zu jeder Anfrage. X-Practice-Name: python-in-practice kennzeichnet diesen Kurs gegenüber dem Dienst. Er ist kein Passwort, und dieses Projekt enthält keine Geheimnisse.

Der Timeout ist nicht optional. Er hat einen Standardwert, aber dieser ist ausdrücklich festgelegt und wird immer übergeben. Eine Anfrage ohne Timeout kann unbegrenzt warten. Lektion 4 behandelt ausschließlich diese Grenze.

Der Parameter session ist besonders interessant und bekommt deshalb einen eigenen Abschnitt.

Warum du der Funktion einen Client übergeben kannst

Sieh dir die erste Zeile noch einmal an:

client = requests if session is None else session

Normalerweise ist session gleich None, und die Funktion verwendet requests direkt. Das passiert, wenn das Programm tatsächlich läuft.

Beim Aufruf lässt sich aber auch etwas anderes mit einer Methode .get() übergeben. Die Funktion verwendet dann dieses Objekt. In einem Test übergibst du ein kleines Ersatzobjekt, das seine Aufrufdaten aufzeichnet und zurückgibt, was du festlegst. Es ist kein Netzwerk beteiligt. Nichts wartet. Das Ergebnis ist jedes Mal gleich.

Halte hier kurz inne, denn dahinter steckt ein wichtiges Konzept, das leicht wie ein bloßer Trick wirkt. Du baust keinen zweiten Weg für Anfragen. Du lässt an genau der Stelle, an der dein Code die Außenwelt berührt, einen austauschbaren Übergang. So kannst du die Außenwelt ersetzen, wenn du untersuchen möchtest, was dein Code getan hat. Jeder Test in diesem Kapitel und im Abschlussprojekt setzt diesen Übergang voraus.

Ein solches Ersatzobjekt muss nicht aufwendig sein. Es braucht eine .get()-Methode, die die übergebenen Daten aufzeichnet und etwas mit einer .json()-Methode zurückgibt. types.SimpleNamespace aus der Standardbibliothek reicht, um eines zusammenzustellen:

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 baut ein Objekt aus den übergebenen Schlüsselwortargumenten. types.SimpleNamespace(get=get) erzeugt also etwas mit einem Attribut .get und sonst nichts. Mehr verlangt request_json nicht.

Probiere es aus:

calls = []
result = request_json("/items", session=fake_session({"items": []}, calls))
print(calls[0])

calls[0] zeigt dir genau die URL, Parameter, Header und den Timeout, die deine Funktion gewählt hat, ohne dass ein einziges Byte durch ein Netzwerk ging.

Probiere auch den echten Dienst aus

Der interne Dienst steht bereit, wenn du eine echte Antwort sehen möchtest:

.venv/bin/python -c "from api_catalog import request_json; print(request_json('/items'))"

Im Moment dekodiert die Funktion den Antwortinhalt unabhängig davon, was der Server gemeldet hat. Wenn der Dienst mit einem Fehler antwortet, gibt die Funktion ihn bereitwillig als vermeintliche Daten weiter. Das ist eine echte Lücke, die Lektion 3 schließt.

Warum akzeptiert request_json eine optionale Session?

Wenn die Idee der Fake-Session noch nicht klar ist, bitte Monty, mit dir durchzugehen, worauf client bei einem normalen Aufruf und bei einem Testaufruf verweist. Bei diesem einen Konzept im Kapitel lohnt es sich, sich Zeit zu nehmen.

Aufgabe

Halte genau requests==2.34.2 in requirements.txt fest. Implementiere request_json als genau eine GET-Anfrage an die fest vorgegebene interne Basisadresse. Verwende die übergebene Session, wenn vorhanden, und übergib immer params, den festen Übungs-Header und einen ausdrücklichen Timeout. Gib vorerst das dekodierte JSON zurück.