Construirea unui API Rest cu Framework Bottle

Publicat: 2022-03-11

API-urile REST au devenit o modalitate obișnuită de a stabili o interfață între back-end-uri web și front-end-uri și între diferite servicii web. Simplitatea acestui tip de interfață și suportul omniprezent al protocoalelor HTTP și HTTPS în diferite rețele și cadre, o fac o alegere ușoară atunci când se iau în considerare problemele de interoperabilitate.

Bottle este un cadru web minimalist Python. Este ușor, rapid și ușor de utilizat și este potrivit pentru construirea de servicii RESTful. O comparație simplă făcută de Andriy Kornatskyy a plasat-o printre primele trei cadre în ceea ce privește timpul de răspuns și debitul (cereri pe secundă). În propriile mele teste pe serverele virtuale disponibile de la DigitalOcean, am descoperit că combinația dintre stiva de servere uWSGI și Bottle ar putea atinge o suprasarcină de 140μs per cerere.

În acest articol, voi oferi o explicație despre cum să construiți un serviciu API RESTful folosind Bottle.

Bottle: Un cadru web Python rapid și ușor

Instalare și configurare

Cadrul Bottle își atinge performanța impresionantă în parte datorită greutății sale reduse. De fapt, întreaga bibliotecă este distribuită ca un modul cu un singur fișier. Aceasta înseamnă că nu vă ține de mână la fel de mult ca alte cadre, dar este, de asemenea, mai flexibil și poate fi adaptat pentru a se potrivi în multe stive tehnice diferite. Prin urmare, Bottle este cel mai potrivit pentru proiectele în care performanța și personalizarea sunt la un nivel superior și în care avantajele de economisire a timpului ale cadrelor mai grele sunt mai puțin luate în considerare.

Flexibilitatea Bottle face ca o descriere aprofundată a configurației platformei să fie puțin inutilă, deoarece este posibil să nu reflecte propriul dvs. stack. Cu toate acestea, o prezentare rapidă a opțiunilor și unde să aflați mai multe despre cum să le configurați este potrivită aici:

Instalare

Instalarea Bottle este la fel de simplă ca și orice alt pachet Python. Opțiunile tale sunt:

  • Instalați pe sistemul dvs. utilizând managerul de pachete al sistemului. Debian Jessie (stable actual) pachetează versiunea 0.12 ca python-bottle .
  • Instalați pe sistemul dvs. utilizând Indexul pachetului Python cu pip install bottle .
  • Instalați într-un mediu virtual (recomandat).

Pentru a instala Bottle într-un mediu virtual, veți avea nevoie de instrumentele virtualenv și pip . Pentru a le instala, vă rugăm să consultați documentația virtualenv și pip, deși probabil că le aveți deja pe sistem.

În Bash, creați un mediu cu Python 3:

 $ virtualenv -p `which python3` env

Suprimarea parametrului -p `which python3` va duce la instalarea interpretului Python implicit prezent pe sistem – de obicei Python 2.7. Python 2.7 este acceptat, dar acest tutorial presupune Python 3.4.

Acum activează mediul și instalează Bottle:

 $ . env/bin/activate $ pip install bottle

Asta e. Sticla este instalată și gata de utilizare. Dacă nu sunteți familiarizat cu virtualenv sau pip , documentația lor este de top. Aruncă o privire! Merită din plin.

Server

Bottle este compatibil cu interfața standard de gateway a serverului web (WSGI) Python, ceea ce înseamnă că poate fi utilizat cu orice server compatibil WSGI. Acestea includ uWSGI, Tornado, Gunicorn, Apache, Amazon Beanstalk, Google App Engine și altele.

Modul corect de configurare variază ușor în funcție de mediu. Bottle expune un obiect care se conformează cu interfața WSGI, iar serverul trebuie configurat pentru a interacționa cu acest obiect.

Pentru a afla mai multe despre cum să vă configurați serverul, consultați documentele serverului și documentele Bottle, aici.

Bază de date

Bottle este independent de baze de date și nu-i pasă de unde provin datele. Dacă doriți să utilizați o bază de date în aplicația dvs., Indexul pachetului Python are mai multe opțiuni interesante, cum ar fi SQLAlchemy, PyMongo, MongoEngine, CouchDB și Boto pentru DynamoDB. Aveți nevoie doar de adaptorul corespunzător pentru ca acesta să funcționeze cu baza de date la alegere.

Elementele de bază ale cadrului sticlei

Acum, să vedem cum să creați o aplicație de bază în Bottle. Pentru exemple de cod, voi presupune Python >= 3.4. Cu toate acestea, majoritatea a ceea ce voi scrie aici va funcționa și pe Python 2.7.

O aplicație de bază în Bottle arată astfel:

 import bottle app = application = bottle.default_app() if __name__ == '__main__': bottle.run(host = '127.0.0.1', port = 8000)

Când spun de bază, vreau să spun că acest program nici măcar nu te „Hello World”. (Când ați accesat ultima dată o interfață REST care a răspuns „Hello World?”) Toate solicitările HTTP către 127.0.0.1:8000 vor primi o stare de răspuns 404 Not Found.

Aplicații în sticlă

Bottle poate avea mai multe aplicații create, dar din motive de comoditate, prima instanță este creată pentru dvs.; aceasta este aplicația implicită. Bottle păstrează aceste instanțe într-o stivă internă a modulului. Ori de câte ori faceți ceva cu Bottle (cum ar fi rularea aplicației sau atașarea unui traseu) și nu specificați despre ce aplicație vorbiți, se referă la aplicația implicită. De fapt, linia app = application = bottle.default_app() nici măcar nu trebuie să existe în această aplicație de bază, dar este acolo, astfel încât să putem invoca cu ușurință aplicația implicită cu Gunicorn, uWSGI sau un server WSGI generic.

Posibilitatea apariției mai multor aplicații poate părea confuză la început, dar adaugă flexibilitate pentru Bottle. Pentru diferite module ale aplicației dvs., puteți crea aplicații Bottle specializate prin instanțierea altor clase Bottle și configurarea lor cu diferite configurații, după cum este necesar. Aceste aplicații diferite ar putea fi accesate prin adrese URL diferite, prin routerul URL al Bottle. Nu vom aprofunda acest lucru în acest tutorial, dar sunteți încurajat să luați o privire la documentația Bottle aici și aici.

Invocarea serverului

Ultima linie a scriptului rulează Bottle folosind serverul indicat. Dacă nu este indicat niciun server, așa cum este cazul aici, serverul implicit este serverul de referință WSGI încorporat din Python, care este potrivit doar pentru scopuri de dezvoltare. Un server diferit poate fi folosit astfel:

 bottle.run(server='gunicorn', host = '127.0.0.1', port = 8000)

Acesta este zahărul sintactic care vă permite să porniți aplicația rulând acest script. De exemplu, dacă acest fișier se numește main.py , puteți pur și simplu să rulați python main.py pentru a porni aplicația. Bottle are o listă destul de extinsă de adaptoare de server care pot fi utilizate în acest fel.

Unele servere WSGI nu au adaptoare Bottle. Acestea pot fi pornite cu propriile comenzi de rulare ale serverului. Pe uWSGI, de exemplu, tot ce ar trebui să faci ar fi să apelezi uwsgi astfel:

 $ uwsgi --http :8000 --wsgi-file main.py

O notă despre structura fișierului

Bottle lasă structura fișierelor aplicației dvs. în întregime la discreția dvs. Am descoperit că politicile mele privind structura fișierelor evoluează de la proiect la proiect, dar tind să se bazeze pe o filozofie MVC.

Creați-vă API-ul REST

Desigur, nimeni nu are nevoie de un server care returnează doar 404 pentru fiecare URI solicitat. Ți-am promis că vom construi un API REST, așa că hai să o facem.

Să presupunem că doriți să construiți o interfață care manipulează un set de nume. Într-o aplicație reală, probabil că ați folosi o bază de date pentru aceasta, dar pentru acest exemplu vom folosi doar structura de date set în memorie.

Scheletul API-ului nostru ar putea arăta astfel. Puteți plasa acest cod oriunde în proiect, dar recomandarea mea ar fi un fișier API separat, cum ar fi api/names.py .

 from bottle import request, response from bottle import post, get, put, delete _names = set() # the set of names @post('/names') def creation_handler(): '''Handles name creation''' pass @get('/names') def listing_handler(): '''Handles name listing''' pass @put('/names/<name>') def update_handler(name): '''Handles name updates''' pass @delete('/names/<name>') def delete_handler(name): '''Handles name deletions''' pass

Dirijare

După cum putem vedea, rutarea în Bottle se face folosind decoratori. Decoratorii importați post , get , put și delete manevrele de registre pentru aceste patru acțiuni. Înțelegerea modului în care aceste lucrări pot fi defalcate după cum urmează:

  • Toți decoratorii de mai sus sunt o comandă rapidă către decoratorii de rutare default_app . De exemplu, decoratorul @get @get() aplică bottle.default_app().get() la handler.
  • Metodele de rutare de pe default_app sunt toate comenzile rapide pentru route() . Deci default_app().get('/') este echivalent cu default_app().route(method='GET', '/') .

Deci @get('/') este același cu @route(method='GET', '/') , care este același cu @bottle.default_app().route(method='GET', '/') , iar acestea pot fi folosite interschimbabil.

Un lucru util despre decoratorul @route este că, dacă doriți, de exemplu, să utilizați același handler pentru a face față atât actualizărilor, cât și ștergerilor de obiecte, puteți transmite o listă de metode pe care le gestionează astfel:

 @route('/names/<name>', method=['PUT', 'DELETE']) def update_delete_handler(name): '''Handles name updates and deletions''' pass

Bine, atunci, să implementăm câțiva dintre acești handlere.

Creați API-ul REST perfect cu Bottle Framework.

API-urile RESTful sunt un element de bază al dezvoltării web moderne. Serviți clienților dvs. API o combinație puternică cu un back-end Bottle.
Tweet

POST: Crearea resurselor

Managerul nostru POST ar putea arăta astfel:

 import re, json namepattern = re.compile(r'^[a-zA-Z\d]{1,64}$') @post('/names') def creation_handler(): '''Handles name creation''' try: # parse input data try: data = request.json() except: raise ValueError if data is None: raise ValueError # extract and validate name try: if namepattern.match(data['name']) is None: raise ValueError name = data['name'] except (TypeError, KeyError): raise ValueError # check for existence if name in _names: raise KeyError except ValueError: # if bad request data, return 400 Bad Request response.status = 400 return except KeyError: # if name already exists, return 409 Conflict response.status = 409 return # add name _names.add(name) # return 200 Success response.headers['Content-Type'] = 'application/json' return json.dumps({'name': name})

Ei bine, asta e destul de mult. Să revizuim acești pași parte cu parte.

Analiza corpului

Acest API cere utilizatorului să POSTĂ un șir JSON la corpul cu un atribut numit „nume”.

Obiectul de request importat mai devreme din bottle indică întotdeauna cererea curentă și deține toate datele solicitării. Atributul său body conține un flux de octeți din corpul cererii, care poate fi accesat de orice funcție care este capabilă să citească un obiect flux (cum ar fi citirea unui fișier).

Metoda request.json() verifică anteturile solicitării pentru tipul de conținut „application/json” și analizează corpul dacă este corect. Dacă Bottle detectează un corp malformat (de exemplu: gol sau cu tip de conținut greșit), această metodă returnează None și astfel generăm o ValueError . Dacă analizatorul JSON detectează conținut JSON incorect; ridică o excepție pe care o surprindem și o ridicăm din nou, din nou ca ValueError .

Analiza și validarea obiectelor

Dacă nu există erori, am convertit corpul cererii într-un obiect Python referit de variabila de data . Dacă am primit un dicționar cu o cheie „nume”, îl vom putea accesa prin data['name'] . Dacă am primit un dicționar fără această cheie, încercarea de a-l accesa ne va duce la o excepție KeyError . Dacă am primit altceva decât un dicționar, vom primi o excepție TypeError . Dacă apare vreuna dintre aceste erori, o ridicăm din nou ca ValueError , indicând o intrare greșită.

Pentru a verifica dacă cheia de nume are formatul corect, ar trebui să o testăm pe o mască regex, cum ar fi masca cu namepattern de nume pe care am creat-o aici. Dacă name cheii nu este un șir, namepattern.match() va genera o TypeError , iar dacă nu se potrivește, va returna None .

Cu masca din acest exemplu, un nume trebuie să fie un alfanumeric ASCII fără spații libere de la 1 la 64 de caractere. Aceasta este o validare simplă și nu testează pentru un obiect cu date de gunoi, de exemplu. Validarea mai complexă și completă poate fi realizată prin utilizarea unor instrumente precum FormEncode.

Testarea existenței

Ultimul test înainte de a îndeplini cererea este dacă numele dat există deja în set. Într-o aplicație mai structurată, acel test ar trebui probabil făcut de un modul dedicat și semnalat către API-ul nostru printr-o excepție specializată, dar deoarece manipulăm un set direct, trebuie să o facem aici.

Semnalăm existența numelui prin ridicarea unui KeyError .

Răspunsuri de eroare

Așa cum obiectul de cerere deține toate datele de solicitare, obiectul de răspuns face același lucru pentru datele de răspuns. Există două moduri de a seta starea răspunsului:

 response.status = 400

și:

 response.status = '400 Bad Request'

Pentru exemplul nostru, am optat pentru forma mai simplă, dar a doua formă poate fi folosită pentru a specifica descrierea textului erorii. Pe plan intern, Bottle va împărți al doilea șir și va seta codul numeric corespunzător.

Răspuns de succes

Dacă toți pașii au succes, îndeplinim cererea adăugând numele la setul _names , setând antetul de răspuns Content-Type și returnând răspunsul. Orice șir returnat de funcție va fi tratat ca corp de răspuns al unui răspuns de 200 Success , așa că pur și simplu generăm unul cu json.dumps .

GET: Lista de resurse

Trecând de la crearea numelui, vom implementa gestionarea listei de nume:

 @get('/names') def listing_handler(): '''Handles name listing''' response.headers['Content-Type'] = 'application/json' response.headers['Cache-Control'] = 'no-cache' return json.dumps({'names': list(_names)})

A enumera numele a fost mult mai ușor, nu-i așa? În comparație cu crearea de nume, nu este mare lucru de făcut aici. Pur și simplu setați niște anteturi de răspuns și returnați o reprezentare JSON a tuturor numelor și am terminat.

PUT: Actualizare resurse

Acum, să vedem cum să implementăm metoda de actualizare. Nu este foarte diferită de metoda create, dar folosim acest exemplu pentru a introduce parametrii URI.

 @put('/names/<oldname>') def update_handler(name): '''Handles name updates''' try: # parse input data try: data = json.load(utf8reader(request.body)) except: raise ValueError # extract and validate new name try: if namepattern.match(data['name']) is None: raise ValueError newname = data['name'] except (TypeError, KeyError): raise ValueError # check if updated name exists if oldname not in _names: raise KeyError(404) # check if new name exists if name in _names: raise KeyError(409) except ValueError: response.status = 400 return except KeyError as e: response.status = e.args[0] return # add new name and remove old name _names.remove(oldname) _names.add(newname) # return 200 Success response.headers['Content-Type'] = 'application/json' return json.dumps({'name': newname})

Schema corpului pentru acțiunea de actualizare este aceeași ca și pentru acțiunea de creare, dar acum avem și un nou parametru oldname în URI, așa cum este definit de ruta @put('/names/<oldname>') .

Parametrii URI

După cum puteți vedea, notația Bottle pentru parametrii URI este foarte simplă. Puteți crea URI-uri cu oricât de mulți parametri doriți. Bottle le extrage automat din URI și le transmite operatorului de solicitare:

 @get('/<param1>/<param2>') def handler(param1, param2): pass

Folosind decoratorii de rute în cascadă, puteți construi URI-uri cu parametri opționali:

 @get('/<param1>') @get('/<param1>/<param2>') def handler(param1, param2 = None) pass

De asemenea, Bottle permite următoarele filtre de rutare în URI:

  • int

Se potrivește numai parametrilor care pot fi convertiți în int și transmite valoarea convertită la handler:

 @get('/<param:int>') def handler(param): pass
  • float

La fel ca int , dar cu valori în virgulă mobilă:

 @get('/<param:float>') def handler(param): pass
  • re (expresii regulate)

Potrivește numai parametrii care se potrivesc cu expresia regulată dată:

 @get('/<param:re:^[az]+$>') def handler(param): pass
  • path

Potrivește subsegmentele căii URI într-un mod flexibil:

 @get('/<param:path>/id>') def handler(param): pass

Chibrituri:

  • /x/id , trecând x ca param .
  • /x/y/id , trecând x/y ca param .

DELETE: Ștergerea resurselor

La fel ca metoda GET, metoda DELETE ne aduce puține noutăți. Doar rețineți că returnarea None fără a seta o stare returnează un răspuns cu un corp gol și un cod de stare 200.

 @delete('/names/<name>') def delete_handler(name): '''Handles name updates''' try: # Check if name exists if name not in _names: raise KeyError except KeyError: response.status = 404 return # Remove name _names.remove(name) return

Pasul final: Activarea API-ului

Presupunând că am salvat API-ul de nume ca api/names.py , acum putem activa aceste rute în fișierul principal al aplicației main.py .

 import bottle from api import names app = application = bottle.default_app() if __name__ == '__main__': bottle.run(host = '127.0.0.1', port = 8000)

Observați că am importat doar modulul de names . Deoarece am decorat toate metodele cu URI-urile lor atașate la aplicația implicită, nu este nevoie să facem alte setări. Metodele noastre sunt deja implementate, gata pentru a fi accesate.

Nimic nu face un front-end fericit ca un API REST bine realizat. Funcționează ca un farmec!

Puteți folosi instrumente precum Curl sau Postman pentru a consuma API-ul și a-l testa manual. (Dacă utilizați Curl, puteți utiliza un formatator JSON pentru a face răspunsul să pară mai puțin aglomerat.)

Bonus: Partajare încrucișată a resurselor (CORS)

Un motiv comun pentru a construi un API REST este comunicarea cu un front-end JavaScript prin AJAX. Pentru unele aplicații, aceste solicitări ar trebui să fie permise să provină din orice domeniu, nu doar din domeniul de origine al API-ului dvs. În mod implicit, majoritatea browserelor nu permit acest comportament, așa că permiteți-mi să vă arăt cum să configurați partajarea resurselor între origini (CORS) în Bottle pentru a permite acest lucru:

 from bottle import hook, route, response _allow_origin = '*' _allow_methods = 'PUT, GET, POST, DELETE, OPTIONS' _allow_headers = 'Authorization, Origin, Accept, Content-Type, X-Requested-With' @hook('after_request') def enable_cors(): '''Add headers to enable CORS''' response.headers['Access-Control-Allow-Origin'] = _allow_origin response.headers['Access-Control-Allow-Methods'] = _allow_methods response.headers['Access-Control-Allow-Headers'] = _allow_headers @route('/', method = 'OPTIONS') @route('/<path:path>', method = 'OPTIONS') def options_handler(path = None): return

Decoratorul de hook ne permite să apelăm o funcție înainte sau după fiecare solicitare. În cazul nostru, pentru a activa CORS, trebuie să setăm antetele Access-Control-Allow-Origin , -Allow-Methods și -Allow-Headers pentru fiecare dintre răspunsurile noastre. Acestea indică solicitantului că vom servi cererile indicate.

De asemenea, clientul poate face o cerere HTTP OPȚIUNI către server pentru a vedea dacă într-adevăr poate face cereri cu alte metode. Cu acest exemplu catch-all, răspundem la toate solicitările OPȚIUNI cu un cod de stare 200 și un corp gol.

Pentru a activa acest lucru, salvați-l și importați-l din modulul principal.

Învelire

Cam despre asta e!

Cu acest tutorial, am încercat să acopăr pașii de bază pentru a crea un API REST pentru o aplicație Python cu cadrul web Bottle.

Vă puteți aprofunda cunoștințele despre acest cadru mic, dar puternic, vizitând tutorialul său și documentele de referință API.

Înrudit : Crearea unui API REST Node.js/TypeScript, partea 1: Express.js