0%

requests en httpx, nu je het weet · eindproject

Eindproject: Project: de leeslijstclient

Sinds hoofdstuk 1 is je client wire.py, één tegelijk gebouwd zodat elk mechanisme zichtbaar bleef. Dit project bouwt de client die een mens echt zou gebruiken, op requests, in reading_client.py:

python reading_client.py add --base-url URL --token TOKEN --title Piranesi --author "Susanna Clarke"
python reading_client.py show 3 --base-url URL

Hier voert pl_http de commando’s voor je uit tegen je server. In hoofdstuk 12 draai je beide op je eigen computer.

Wat de client moet doen

SERVICE_CONTRACT.md heeft een nieuwe sectie, “The client”. Lees die eerst; het is de eis. Kort gezegd: vijf commando’s, list, show ID, add, update ID en delete ID, elk één verzoek aan de service; --token voor schrijfacties; een time-out van TIMEOUT, (1.0, 2.0), bij elk verzoek; resultaten op standaarduitvoer; en een exitstatus die zegt wat er gebeurde: 0 als het verzoek slaagde, 1 als er geen antwoord kwam, 2 als de server met een fout antwoordde of de de regels breekt.

reading_client.py bevat al de delen die niet over gaan. build_parser() en check_usage(parser, args) lezen en controleren de commandoregel, book_url(base_url, book_id) bouwt /books of /books/ID op de basis-, en book_line(book) maakt de opmaak ID: TITLE by AUTHOR. Je make_session() uit les 1 staat er ook. Er zijn nog vier functies over.

Eén functie stuurt elk verzoek

Elk commando is één verzoek met een paar delen, dus één functie stuurt ze allemaal:

def call(method, url, *, session=None, token=None, data=None, timeout=TIMEOUT):

Gebruik client = requests if session is None else session, zoals in les 2, en verstuur met client.request(method, url, ...), dat de als tekst neemt. Voeg daarna alleen toe wat het verzoek nodig heeft. Met een token een header authorization: Bearer TOKEN, gebouwd uit de token die je kreeg, en zonder token geen zo’n header. Met data json=data, waardoor requests de codeert en content-type: application/json toevoegt; leesacties sturen geen body. Altijd timeout=timeout.

vragen niets van jou. requests volgt ze, zoals les 2 liet zien, en de oude /v0-paden van de service beantwoorden elke methode met 308, wat de methode en de body behoudt. Een PUT via --base-url http://books.test/v0 komt bij /books/ID aan als een PUT, met zijn body en zijn authorization-header, omdat requests die header behoudt wanneer de doorverwijzing op dezelfde host blijft.

Eerst de status, dan de uitvoer

Een antwoord is een succes of een fout volgens zijn klasse, de regel uit hoofdstuk 3: 2xx is een resultaat om af te drukken, en elke 4xx of 5xx is een fout. Beslis dat voordat je iets afdrukt. Druk bij een fout één regel af op standaardfout, print(describe_error(response), file=sys.stderr), druk niets af op standaarduitvoer, en geef 2 terug. Standaarduitvoer draagt alleen resultaten, zodat een programma dat hem leest een foutmelding nooit voor een boek aanziet.

describe_error(response) geeft die ene regel terug: de status, een spatie, daarna de error-tekst van de server als de body een - is dat er een heeft, of de reason phrase als dat niet zo is. Een 503 van een proxy heeft misschien helemaal geen JSON-body, dus response.json() kan een opwerpen; hij werpt requests.JSONDecodeError op, een subklasse van ValueError, die je afvangt met except ValueError:.

delete 7 krijgt 403. Wat doet de client?

Als er geen antwoord komt, is er geen antwoord om naar te kijken: requests werpt in plaats daarvan een exception op. Twee klassen doen ertoe, en hun volgorde doet ertoe. Een requests.ConnectTimeout is zowel een requests.Timeout als een requests.ConnectionError, en Python voert de eerste except-clausule uit die past. Vang dus eerst requests.Timeout af en druk no answer from BASE_URL in time af, daarna requests.ConnectionError en druk could not connect to BASE_URL af, elk op standaardfout, elk met 1 als . Vang niets breders af. Een except Exception: zou een bug in je eigen code melden als een netwerkprobleem.

Waarom vang je requests.Timeout af vóór requests.ConnectionError?

Mijlpalen

  1. call, zoals hierboven. De voorbeeldtest in tests/test_client.py slaagt zodra hij bestaat.

  2. describe_error(response), zoals hierboven.

  3. run(args, *, session) voert één gecontroleerd commando uit. Kies de methode voor args.command, bouw {"title": args.title, "author": args.author} voor add en update, verstuur het met call(..., session=session, token=args.token, data=data), en controleer eerst de status. Bij succes drukt list één book_line af per boek in response.json()["books"], drukt show de regel van het boek af, drukken add en update er added of updated voor af, en drukt delete deleted ID af; een 204 heeft geen body, dus delete leest er geen. Geef 0 terug.

  4. main(argv=None, *, session=None) parset met build_parser(), roept check_usage(parser, args) aan en voert het commando uit binnen try. Zonder sessie maakt hij er een, with make_session() as session:, zodat de sessie gesloten wordt wanneer het commando eindigt. Samen ongeveer veertig regels.

Een commandoregel die het contract weigert, bereikt run nooit. Probeer er een in de , en vraag de shell daarna om de exitstatus van het laatste commando:

python reading_client.py show --base-url http://books.test
echo $?

argparse drukt het gebruik en reading_client.py: error: show needs a book id af op standaardfout, en echo $? drukt 2 af. Er wordt geen verzoek verstuurd, dus er is geen server nodig.

Draai daarna het scenario:

python pl_http.py client

Het voert acht commando’s uit tegen je server, op één sessie die pl_http daar koppelt. De update met de token van een lezer gaat via het oude pad, wordt doorverwezen en wordt geweigerd:

request 1  PUT /v0/books/3  ->  308 Permanent Redirect  (connection 1)
request 2  PUT /books/3  ->  403 Forbidden  (connection 1)
--- your reading_client.py update 3 --base-url http://books.test/v0 --token tok-ben-reads-K9pz --title "Piranesi (paperback)" --author "Susanna Clarke" ---
exit status: 2
standard output: []
standard error: ['403 this token may read the list but not change it']

Het laatste commando gebruikt een tweede sessie waarvan de handler naar een wijst waar niets luistert. Opgenomen op een Mac kwam de weigering meteen en gaf standard error: ['could not connect to http://books.test'] met exitstatus 1; als een systeem de verbinding in plaats daarvan liet wachten, zou je time-outregel verschijnen.

Draai ook je tests, nu call bestaat:

python -m pytest -q

Eigen tests toevoegen aan tests/test_client.py is de moeite waard. fakes.fake_session antwoordt net zo makkelijk met een fout als met een boek, en capsys van pytest laat zien wat main afdrukte:

def test_a_404_exits_2(capsys):
    calls = []
    session = fakes.fake_session([fakes.fake_response(404, {"error": "no book has that id"})], calls)
    status = reading_client.main(["show", "9", "--base-url", "http://books.test"], session=session)
    assert status == 2
    assert capsys.readouterr().out == ""

Klik op Check my work. Je client doet nu de clienthelft van alles wat de server met de hand doet: doorverwijzingen, inloggegevens, framing en de verbinding. Hoofdstuk 11 kijkt naar de andere kant en vraagt wat een webframework voor je server zou doen.

Opdracht

Bouw reading_client.py, een commandoregelclient voor de leeslijstservice, zoals SERVICE_CONTRACT.md die beschrijft onder “The client”.

Schrijf vier ; houd de meegeleverde build_parser, check_usage, book_url, book_line en je make_session:

  1. call(method, url, *, session=None, token=None, data=None, timeout=TIMEOUT) stuurt één verzoek met client = requests if session is None else session en geeft het antwoord terug. Hij stuurt authorization: Bearer TOKEN alleen als token gegeven is, json=data alleen als data gegeven is, en altijd timeout=timeout. Hij geeft nooit allow_redirects mee.

  2. describe_error(response) geeft één regel terug: de status, een spatie, daarna de error-tekst van de server, of de reason phrase als de body er geen heeft.

  3. run(args, *, session) stuurt het verzoek van het commando via call met args.token, stuurt {"title": ..., "author": ...} voor add en update, en controleert eerst de status: bij elke 4xx of 5xx drukt hij describe_error(response) af op standaardfout, drukt niets af op standaarduitvoer en geeft 2 terug. Anders drukt hij de resultaatregel of -regels uit het contract af op standaarduitvoer en geeft 0 terug.

  4. main(argv=None, *, session=None) parset argv met build_parser(), roept check_usage(parser, args) aan en voert het commando uit, op session als die gegeven is en anders binnen with make_session() as session:. Hij vangt eerst requests.Timeout af, daarna requests.ConnectionError, drukt voor elk van beide één regel af op standaardfout en geeft 1 terug. Hij vangt niets breders af.

In de voert python pl_http.py client acht commando’s uit tegen je server, twee ervan via de oude /v0-paden, en toont voor elk commando de exitstatus, de standaarduitvoer en de standaardfout. python -m pytest -q draait je tests.

Klik daarna op Check my work. Met een basis-, boeken, titels en tokens die je niet hebt gezien, voert die elk commando uit via een sessie die elke aanroep opschrijft en uit een antwoordt: de , de URL, de -body, de authorization-header alleen met een token, en de time-out van elk verzoek; de exacte uitvoerregels; één regel op standaardfout die de status noemt en exitstatus 2 voor antwoorden 404, 409, 500 en 503; exitstatus 1 voor een verbindingstime-out, een leestime-out en een geweigerde verbinding, waarbij een time-out anders gemeld wordt dan een geweigerde verbinding; een gewone die niet wordt afgevangen; en status 2 van argparse voor een die het contract weigert. Daarna voert hij add, update met de token van een lezer en met die van een schrijver, en delete uit via de oude /v0-paden van een kleine server binnen de controle, in het geheugen, en show van het verwijderde boek, en controleert hij dat main zonder sessie er een maakt en die sluit. Ten slotte draait hij je tests één keer; ze moeten allemaal slagen. Niets uit je terminal wordt beoordeeld.