0%

Anfrage-Bodys und eine JSON-API · Abschlussprojekt

Abschlussprojekt: Projekt: Eine beschreibbare Leseliste

Deine Leseliste legt jetzt Bücher an, liest, ersetzt und löscht sie, lehnt jede fehlerhafte Anfrage mit ihrem eigenen Status ab und hat Tests, die sie an ihre Methodenversprechen binden. Eine Zeile von SERVICE_CONTRACT.md stimmt noch nicht: GET /books?limit=N, die ersten N Bücher. Dieses Projekt fügt sie hinzu und prüft dann den ganzen Dienst gegen den Vertrag.

Was der Vertrag verlangt

Öffne SERVICE_CONTRACT.md und lies ihn noch einmal als Checkliste. Jede Zeile außer ?limit= sollte bereits gelten:

  • GET und HEAD auf /books und /books/{id}; POST /books mit 201 und location; PUT /books/{id} mit 200; DELETE /books/{id} mit 204, ohne Body und ohne content-length.

  • 405 mit allow: GET, HEAD, POST auf der Sammlung und allow: GET, HEAD, PUT, DELETE auf einem Element, und 404 überall sonst.

  • Die Fehlerreihenfolge: Route und , Rahmung (411, 400, 413), die Buch-ID, Medientyp (415), Syntax (400), Form und (422), wobei bei keinem Fehler etwas gespeichert oder hochgezählt wird. read_body prüft die Rahmung, bevor handle läuft, also bekommt ein POST /nowhere ohne Länge trotzdem 411.

Starte das vollständige Szenario des Hilfsprogramms, um zu sehen, wo du stehst:

python pl_http.py crud

Es fragt nach GET /books?limit=1, legt Buch 3 an, ersetzt es, löscht es und fragt nach GET /books?limit=0. Heute antworten beide GETs mit 200 und allen Büchern, content-length: 132 für die zwei Beispielbücher, weil handle die Query ignoriert. Die drei Schreibzugriffe dazwischen sollten bereits HTTP/1.0 201 Created, HTTP/1.0 200 OK und HTTP/1.0 204 No Content zeigen.

Ein Query-Parameter

In Kapitel 2 hast du Querys mit urlencode gebaut und mit urlsplit zerlegt. Eine Query auf dem Server zu lesen ist das Umgekehrte: urlsplit(target).query ist der Text nach dem ?, und parse_qs macht daraus ein . Jeder Name verweist auf eine von Werten, weil ein Name mehr als einmal vorkommen kann; parse_qs dekodiert außerdem %-Escapes und macht aus + ein Leerzeichen, wie du es bei Formularen gesehen hast:

Try it

Das gibt {'limit': ['2'], 'sort': ['title']} aus, dann {'limit': ['1', '2']}, dann {}. parse_qs ist oben in reading_server.py bereits importiert.

Die Regeln für limit folgen denen, die read_body auf eine Länge anwendet. Kein limit heißt keine Begrenzung. Ein limit muss genau einmal vorkommen und aus ASCII-Ziffern bestehen, geprüft mit isascii() und isdigit() vor int, damit two, -1, 1.5 und ² abgelehnt werden, statt deinen Server abstürzen zu lassen. Und es muss zwischen 1 und 100 liegen: limit=0 fragt nach nichts, und ein Client, der zehntausend Bücher in einer Antwort will, sollte sie nicht bekommen. Jedes Limit, das eine Regel verletzt, bekommt 400 mit einem -Fehler. Andere Query-, etwa sort=title, werden ignoriert.

query_limit(target) macht aus diesen Regeln einen einzigen Wert für handle: 0 für keine Begrenzung, die Zahl für ein gültiges Limit und None für ein ungültiges. 0 funktioniert als „keine Begrenzung“, weil ein gültiges Limit nie 0 ist.

Eine echte API kombiniert ein Limit meist mit einer Möglichkeit, nach der nächsten Seite zu fragen; solche Entwürfe gehen über diesen Kurs hinaus. Dieser hört bei den ersten N auf.

Meilensteine

  1. Schreib query_limit(target) mit den Regeln von oben.

  2. Ruf in handle im Zweig für GET und HEAD auf /books query_limit(request["target"]) auf. Wenn es None zurückgibt, antworte mit json_error(400, ...). Andernfalls bau die Liste wie bisher, nach ID aufsteigend, und hör auf, sobald sie limit Bücher enthält, wenn limit nicht 0 ist. Die HEAD-Regel am Ende von handle leert weiterhin den Body, also trägt HEAD /books?limit=1 die Länge, die GET /books?limit=1 senden würde.

  3. Führ python pl_http.py crud noch einmal aus. Die erste Antwort ist jetzt HTTP/1.0 200 OK mit content-length: 73, ein Buch, und die letzte ist HTTP/1.0 400 Bad Request.

  4. Führ python -m pytest -q aus. Deine Tests aus Lektion 6 sollten weiterhin bestehen. Du kannst eigene Tests für ?limit= hinzufügen, auf dieselbe Weise geschrieben, zum Beispiel:

def test_a_limit_lists_the_first_books():
    response = reading_server.handle(make_request("GET", "/books?limit=1"), reading_server.sample_state())
    data = json.loads(response["body"].decode("utf-8"))
    assert len(data["books"]) == 1
python -m pytest -q

Was geprüft wird

Check my work prüft noch einmal das ganze Kapitel, mit Büchern, die es dir nicht gezeigt hat. Es ruft read_body, query_limit und handle direkt auf, mit guten und schlechten Limits, jeder Art von Schreibzugriff und einer fehlerhaften Anfrage pro Ursache, und vergleicht den Zustand vor und nach jedem Fehler. Es baut deinen Server mit make_server, ohne einen zu öffnen, schickt ein POST, ein DELETE und ein POST /nowhere ohne Länge als Bytes durch die Handler-Klasse und liest die Bytes, die zurückkommen. Es führt serve mit einem Ersatz-Server aus, und es führt deine Tests einmal gegen sein eigenes korrektes handle aus.

Drück dann Check my work. Dein Dienst hält jetzt seinen ganzen Vertrag. Manche Clients fragen allerdings noch unter Pfaden aus einer älteren Version der API nach Büchern, /v0/books; in Kapitel 6 sagt dein Server ihnen, wohin die Bücher gegangen sind, und dein Client lernt, zu folgen.

Aufgabe

Stell die beschreibbare Leseliste fertig: Füge GET /books?limit=N hinzu und sorg dafür, dass jede Zeile von SERVICE_CONTRACT.md gilt.

In reading_server.py:

  1. Schreib query_limit(target). Lies die Query mit parse_qs(urlsplit(target).query). Gib 0 zurück, wenn es kein limit gibt. Gib None zurück, wenn limit mehr als einmal vorkommt, nicht aus ASCII-Ziffern besteht oder nicht zwischen 1 und 100 liegt. Andernfalls gib das Limit als int zurück.

  2. In handle, bei GET und HEAD auf /books: Wenn query_limit None zurückgibt, antworte mit 400 über json_error; andernfalls die ersten limit Bücher nach ID aufsteigend auf, oder jedes Buch, wenn das Limit 0 ist. Andere Query- werden ignoriert.

Alles aus den Lektionen 1 bis 6 muss weiterhin funktionieren: read_body, das von make_server an den Adapter übergeben wird, body_error, die drei Schreibzugriffe mit 201 und location, 200 und 204 ohne Body oder content-length, Status nach Ursache ohne Änderung am Zustand bei einem Fehler, die allow- und deine Tests in tests/test_server.py.

Im führt python pl_http.py crud eine vollständige Abfolge gegen deinen Server aus, und python -m pytest -q führt deine Tests aus.

Drück dann Check my work. Mit Büchern, IDs und Limits, die du noch nicht gesehen hast, ruft es query_limit, read_body und handle direkt auf, für gültige und ungültige Limits, HEAD mit einem Limit, jeden Schreibzugriff und eine fehlerhafte Anfrage pro Ursache, und vergleicht den Zustand vor und nach jedem Fehler. Es baut deinen Server mit make_server, ohne einen zu öffnen, und schickt ein POST, ein DELETE und ein POST /nowhere ohne Länge als Bytes durch die Handler-Klasse. Es führt serve mit einem Ersatz-Server aus. Zuletzt führt es deine Tests einmal mit seinem eigenen korrekten handle aus; sie müssen alle bestehen. Nichts aus deinem Terminal wird bewertet.