Budowanie Rest API za pomocą Bottle Framework

Opublikowany: 2022-03-11

Interfejsy API REST stały się powszechnym sposobem ustanowienia interfejsu między back-endami i front-endami sieci Web oraz między różnymi usługami internetowymi. Prostota tego rodzaju interfejsu i wszechobecna obsługa protokołów HTTP i HTTPS w różnych sieciach i platformach sprawia, że ​​jest to łatwy wybór przy rozważaniu kwestii interoperacyjności.

Bottle to minimalistyczny framework sieciowy Pythona. Jest lekki, szybki i łatwy w użyciu oraz dobrze nadaje się do tworzenia usług RESTful. Podstawowe porównanie przeprowadzone przez Andrija Kornatskyy'ego umieściło go wśród trzech najlepszych frameworków pod względem czasu odpowiedzi i przepustowości (żądań na sekundę). W moich własnych testach na wirtualnych serwerach dostępnych w DigitalOcean odkryłem, że połączenie stosu serwerów uWSGI i Bottle może osiągnąć nawet 140 μs narzutu na żądanie.

W tym artykule przedstawię przewodnik, jak zbudować usługę RESTful API przy użyciu Bottle.

Butelka: szybki i lekki framework WWW w Pythonie

Instalacja i konfiguracja

Szkielet Butelki osiąga imponującą wydajność po części dzięki niewielkiej wadze. W rzeczywistości cała biblioteka jest dystrybuowana jako moduł jednoplikowy. Oznacza to, że nie trzyma się tak mocno, jak inne frameworki, ale jest również bardziej elastyczny i można go dostosować do wielu różnych stosów technologicznych. Dlatego Bottle najlepiej nadaje się do projektów, w których wydajność i możliwość dostosowywania są na wagę złota, a oszczędność czasu dzięki bardziej wytrzymałym strukturom jest mniej ważna.

Elastyczność Bottle sprawia, że ​​szczegółowy opis konfiguracji platformy jest nieco bezcelowy, ponieważ może nie odzwierciedlać twojego własnego stosu. Jednak krótki przegląd opcji i gdzie można dowiedzieć się więcej o tym, jak je skonfigurować, jest odpowiedni tutaj:

Instalacja

Instalacja Bottle jest tak prosta, jak instalowanie jakiegokolwiek innego pakietu Pythona. Twoje opcje to:

  • Zainstaluj w swoim systemie za pomocą menedżera pakietów systemu. Debian Jessie (aktualna stabilna) pakuje wersję 0.12 jako python-bottle .
  • Zainstaluj w swoim systemie za pomocą indeksu pakietów Pythona za pomocą pip install bottle .
  • Zainstaluj w środowisku wirtualnym (zalecane).

Aby zainstalować Bottle w środowisku wirtualnym, będziesz potrzebować narzędzi virtualenv i pip . Aby je zainstalować, zapoznaj się z dokumentacją virtualenv i pip, chociaż prawdopodobnie masz je już w swoim systemie.

W Bash utwórz środowisko za pomocą Pythona 3:

 $ virtualenv -p `which python3` env

Pominięcie parametru -p `which python3` doprowadzi do instalacji domyślnego interpretera Pythona obecnego w systemie – zwykle Pythona 2.7. Obsługiwany jest Python 2.7, ale w tym samouczku założono Python 3.4.

Teraz aktywuj środowisko i zainstaluj Butelkę:

 $ . env/bin/activate $ pip install bottle

Otóż ​​to. Butelka jest zainstalowana i gotowa do użycia. Jeśli nie znasz virtualenv lub pip , ich dokumentacja jest na najwyższym poziomie. Spójrz! Są tego warte.

serwer

Bottle jest zgodny ze standardowym interfejsem Web Server Gateway Interface (WSGI) Pythona, co oznacza, że ​​może być używany z dowolnym serwerem zgodnym z WSGI. Obejmuje to uWSGI, Tornado, Gunicorn, Apache, Amazon Beanstalk, Google App Engine i inne.

Prawidłowy sposób konfiguracji różni się nieznacznie w zależności od środowiska. Butelka udostępnia obiekt zgodny z interfejsem WSGI, a serwer musi być skonfigurowany do interakcji z tym obiektem.

Aby dowiedzieć się więcej o tym, jak skonfigurować serwer, zapoznaj się z dokumentacją serwera oraz z dokumentacją Bottle, tutaj.

Baza danych

Bottle jest niezależny od bazy danych i nie dba o to, skąd pochodzą dane. Jeśli chcesz używać bazy danych w swojej aplikacji, Python Package Index ma kilka interesujących opcji, takich jak SQLAlchemy, PyMongo, MongoEngine, CouchDB i Boto dla DynamoDB. Potrzebujesz tylko odpowiedniego adaptera, aby działał z wybraną bazą danych.

Podstawy struktury butelek

Zobaczmy teraz, jak zrobić podstawową aplikację w Butelce. Dla przykładów kodu przyjmę Python >= 3.4. Jednak większość tego, co tutaj napiszę, będzie działać również w Pythonie 2.7.

Podstawowa aplikacja w Butelce wygląda tak:

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

Kiedy mówię podstawowy, mam na myśli to, że ten program nawet cię nie "Hello World". (Kiedy ostatni raz uzyskałeś dostęp do interfejsu REST, który odpowiedział „Hello World?”) Wszystkie żądania HTTP kierowane do 127.0.0.1:8000 otrzymają stan odpowiedzi 404 Not Found.

Aplikacje w butelce

Butelka może mieć utworzonych kilka instancji aplikacji, ale dla wygody tworzona jest pierwsza instancja; to jest domyślna aplikacja. Butelka przechowuje te instancje w stosie wewnętrznym modułu. Za każdym razem, gdy robisz coś z Bottle (np. uruchamiasz aplikację lub dołączasz trasę) i nie określasz, o której aplikacji mówisz, odnosi się to do aplikacji domyślnej. W rzeczywistości linia app = application = bottle.default_app() nawet nie musi istnieć w tej podstawowej aplikacji, ale jest tam, abyśmy mogli łatwo wywołać domyślną aplikację za pomocą Gunicorn, uWSGI lub jakiegoś ogólnego serwera WSGI.

Możliwość wielu aplikacji może początkowo wydawać się myląca, ale dodają one elastyczności Butelce. W przypadku różnych modułów aplikacji możesz tworzyć wyspecjalizowane aplikacje Bottle, tworząc instancje innych klas Bottle i konfigurując je z różnymi konfiguracjami zgodnie z potrzebami. Dostęp do tych różnych aplikacji można uzyskać pod różnymi adresami URL, za pośrednictwem routera URL w Butelce. Nie będziemy zagłębiać się w to w tym samouczku, ale zachęcamy do zapoznania się z dokumentacją Bottle tutaj i tutaj.

Wywołanie serwera

Ostatnia linia skryptu uruchamia Butelkę na wskazanym serwerze. Jeśli nie wskazano żadnego serwera, jak w tym przypadku, domyślnym serwerem jest wbudowany serwer referencyjny WSGI Pythona, który nadaje się tylko do celów programistycznych. Inny serwer może być używany w ten sposób:

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

To jest cukier składniowy, który pozwala uruchomić aplikację, uruchamiając ten skrypt. Na przykład, jeśli ten plik ma nazwę main.py , możesz po prostu uruchomić python main.py , aby uruchomić aplikację. Bottle zawiera dość obszerną listę adapterów serwerowych, które można w ten sposób wykorzystać.

Niektóre serwery WSGI nie mają adapterów do butelek. Można je uruchomić za pomocą własnych poleceń uruchamiania serwera. Na przykład w uWSGI wystarczyłoby zadzwonić do uwsgi w następujący sposób:

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

Uwaga na temat struktury plików

Bottle pozostawia strukturę plików Twojej aplikacji całkowicie do Ciebie. Odkryłem, że moje zasady dotyczące struktury plików ewoluują od projektu do projektu, ale zwykle opierają się na filozofii MVC.

Budowanie własnego interfejsu API REST

Oczywiście nikt nie potrzebuje serwera, który zwraca tylko 404 dla każdego żądanego URI. Obiecałem, że zbudujemy REST API, więc zróbmy to.

Załóżmy, że chcesz zbudować interfejs, który manipuluje zbiorem nazw. W prawdziwej aplikacji prawdopodobnie użyjesz do tego bazy danych, ale w tym przykładzie użyjemy po prostu struktury danych w set pamięci.

Szkielet naszego API może wyglądać tak. Możesz umieścić ten kod w dowolnym miejscu projektu, ale moją rekomendacją byłby osobny plik API, taki jak 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

Rozgromienie

Jak widać, trasowanie w Bottle odbywa się za pomocą dekoratorów. Zaimportowane dekoratory post , get , put i delete dla tych czterech akcji . Zrozumienie, jak te prace można podzielić w następujący sposób:

  • Wszystkie powyższe dekoratory są skrótem do dekoratorów routingu default_app . Na przykład dekorator @get @get() stosuje bottle.default_app().get() do procedury obsługi.
  • Wszystkie metody routingu w default_app są skrótami dla route() . Tak więc default_app().get('/') jest równoważne default_app().route(method='GET', '/') .

Zatem @get('/') jest tym samym co @route(method='GET', '/') , co jest tym samym co @bottle.default_app().route(method='GET', '/') , które mogą być używane zamiennie.

Jedną z przydatnych rzeczy w dekoratorze @route jest to, że jeśli chcesz, na przykład, używać tego samego modułu obsługi do obsługi zarówno aktualizacji, jak i usuwania obiektów, możesz po prostu przekazać listę metod, które obsługuje w następujący sposób:

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

W porządku, zaimplementujmy więc niektóre z tych programów obsługi.

Stwórz swój doskonały interfejs API REST za pomocą Bottle Framework.

RESTful API to podstawa nowoczesnego tworzenia stron internetowych. Zaserwuj swoim klientom API potężną miksturę z zapleczem Butelki.
Ćwierkać

POST: Tworzenie zasobów

Nasz program obsługi POST może wyglądać tak:

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

Cóż, to całkiem dużo. Przyjrzyjmy się tym krokom krok po kroku.

Parsowanie ciała

Ten interfejs API wymaga od użytkownika POST ciągu JSON w treści z atrybutem o nazwie „name”.

Obiekt request zaimportowany wcześniej z bottle zawsze wskazuje na bieżące żądanie i przechowuje wszystkie dane żądania. Jego atrybut body zawiera strumień bajtów treści żądania, do którego można uzyskać dostęp za pomocą dowolnej funkcji, która jest w stanie odczytać obiekt strumienia (tak jak czytanie pliku).

Metoda request.json() sprawdza nagłówki żądania pod kątem typu zawartości „application/json” i analizuje treść, jeśli jest poprawna. Jeśli Bottle wykryje nieprawidłowo sformułowaną treść (np.: pustą lub z niewłaściwym typem treści), ta metoda zwraca None i dlatego ValueError . Jeśli nieprawidłowo sformułowana zawartość JSON zostanie wykryta przez parser JSON; zgłasza wyjątek, który przechwytujemy i ponownie podnosimy, ponownie jako ValueError .

Parsowanie i walidacja obiektów

Jeśli nie ma błędów, przekonwertowaliśmy treść żądania na obiekt Pythona, do którego odwołuje się zmienna data . Jeśli otrzymaliśmy słownik z kluczem „name”, będziemy mogli uzyskać do niego dostęp poprzez data['name'] . Jeśli otrzymaliśmy słownik bez tego klucza, próba uzyskania do niego dostępu doprowadzi nas do wyjątku KeyError . Jeśli otrzymaliśmy coś innego niż słownik, otrzymamy wyjątek TypeError . Jeśli wystąpi którykolwiek z tych błędów, ponownie zgłaszamy go jako ValueError , wskazując na złe dane wejściowe.

Aby sprawdzić, czy klucz nazwy ma właściwy format, powinniśmy przetestować go z maską wyrażeń regularnych, taką jak maska namepattern , którą tutaj stworzyliśmy. Jeśli name klucza nie jest ciągiem, namepattern.match() zwróci TypeError , a jeśli nie będzie pasować, zwróci None .

W przypadku maski w tym przykładzie nazwa musi być alfanumeryczną ASCII bez spacji od 1 do 64 znaków. Jest to prosta walidacja i nie testuje na przykład obiektu z danymi typu garbage. Bardziej złożoną i kompletną walidację można osiągnąć za pomocą narzędzi takich jak FormEncode.

Testowanie na istnienie

Ostatnim testem przed spełnieniem żądania jest to, czy dana nazwa już istnieje w zestawie. W bardziej ustrukturyzowanej aplikacji test ten powinien prawdopodobnie zostać wykonany przez dedykowany moduł i zasygnalizowany naszemu API przez wyspecjalizowany wyjątek, ale ponieważ manipulujemy zestawem bezpośrednio, musimy to zrobić tutaj.

Sygnalizujemy istnienie nazwy przez podniesienie KeyError .

Odpowiedzi na błędy

Tak jak obiekt żądania przechowuje wszystkie dane żądania, obiekt odpowiedzi robi to samo dla danych odpowiedzi. Istnieją dwa sposoby ustawienia statusu odpowiedzi:

 response.status = 400

oraz:

 response.status = '400 Bad Request'

W naszym przykładzie zdecydowaliśmy się na prostszy formularz, ale drugi formularz może być użyty do określenia tekstu opisu błędu. Wewnętrznie Bottle podzieli drugi ciąg i odpowiednio ustawi kod numeryczny.

Pomyślna odpowiedź

Jeśli wszystkie kroki powiodą się, realizujemy żądanie, dodając nazwę do zestawu _names , ustawiając nagłówek odpowiedzi Content-Type i zwracając odpowiedź. Każdy ciąg znaków zwrócony przez funkcję będzie traktowany jako treść odpowiedzi odpowiedzi 200 Success , więc po prostu generujemy ją za pomocą json.dumps .

POBIERZ: Lista zasobów

Przechodząc od tworzenia nazw, zaimplementujemy procedurę obsługi listy nazw:

 @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)})

Wymienianie nazwisk było o wiele łatwiejsze, prawda? W porównaniu z tworzeniem nazw nie ma tu wiele do zrobienia. Po prostu ustaw kilka nagłówków odpowiedzi i zwróć reprezentację wszystkich nazw w formacie JSON i gotowe.

PUT: Aktualizacja zasobów

Zobaczmy teraz, jak zaimplementować metodę aktualizacji. Nie różni się zbytnio od metody create, ale używamy tego przykładu do wprowadzenia parametrów 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})

Schemat treści dla akcji aktualizacji jest taki sam jak dla akcji tworzenia, ale teraz mamy również nowy parametr stara nazwa w identyfikatorze URI, zdefiniowany przez trasę oldname @put('/names/<oldname>') .

Parametry URI

Jak widać, notacja Bottle'a dotycząca parametrów URI jest bardzo prosta. Możesz tworzyć identyfikatory URI z dowolną liczbą parametrów. Butelka automatycznie wyodrębnia je z identyfikatora URI i przekazuje do obsługi żądań:

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

Używając dekoratorów tras kaskadowych, możesz budować identyfikatory URI z opcjonalnymi parametrami:

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

Ponadto Butelka umożliwia stosowanie następujących filtrów routingu w identyfikatorach URI:

  • int

Dopasowuje tylko parametry, które można przekonwertować na int , i przekazuje przekonwertowaną wartość do modułu obsługi:

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

To samo co int , ale z wartościami zmiennoprzecinkowymi:

 @get('/<param:float>') def handler(param): pass
  • re (wyrażenia regularne)

Dopasowuje tylko parametry, które pasują do podanego wyrażenia regularnego:

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

Dopasowuje podsegmenty ścieżki URI w elastyczny sposób:

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

Mecze:

  • /x/id , przekazując x jako param .
  • /x/y/id , przekazując x/y jako param .

USUŃ: usuwanie zasobów

Podobnie jak metoda GET, metoda DELETE przynosi nam niewiele wiadomości. Zauważ tylko, że zwrócenie None bez ustawienia statusu zwraca odpowiedź z pustą treścią i kodem statusu 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

Ostatni krok: aktywacja API

Przypuśćmy, że zapisaliśmy nasze API nazw jako api/names.py , możemy teraz włączyć te trasy w głównym pliku aplikacji 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)

Zauważ, że zaimportowaliśmy tylko moduł names . Ponieważ ozdobiliśmy wszystkie metody ich identyfikatorami URI dołączonymi do domyślnej aplikacji, nie ma potrzeby wykonywania dalszych ustawień. Nasze metody są już gotowe do użycia.

Nic tak nie uszczęśliwia front-endu jak dobrze wykonane REST API. Działa jak marzenie!

Możesz użyć narzędzi takich jak Curl lub Postman, aby wykorzystać interfejs API i przetestować go ręcznie. (Jeśli używasz Curl, możesz użyć programu formatującego JSON, aby odpowiedź wyglądała na mniej zaśmieconą).

Bonus: Udostępnianie zasobów między źródłami (CORS)

Jednym z typowych powodów budowania interfejsu API REST jest komunikacja z interfejsem JavaScript za pośrednictwem AJAX. W przypadku niektórych aplikacji żądania te powinny pochodzić z dowolnej domeny, a nie tylko domeny macierzystej Twojego interfejsu API. Domyślnie większość przeglądarek nie zezwala na to zachowanie, więc pokażę ci, jak skonfigurować udostępnianie zasobów między źródłami (CORS) w programie Bottle, aby to umożliwić:

 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

Dekorator hook pozwala nam wywołać funkcję przed lub po każdym żądaniu. W naszym przypadku, aby włączyć CORS, musimy ustawić nagłówki Access-Control-Allow-Origin , -Allow-Methods i -Allow-Headers dla każdej z naszych odpowiedzi. Wskazują one żądającemu, że będziemy obsługiwać wskazane żądania.

Ponadto klient może wysłać żądanie HTTP OPTIONS do serwera, aby sprawdzić, czy naprawdę może wysyłać żądania innymi metodami. W tym przykładowym przykładzie typu catch-all odpowiadamy na wszystkie żądania OPTIONS kodem statusu 200 i pustą treścią.

Aby to umożliwić, po prostu zapisz go i zaimportuj z modułu głównego.

Zakończyć

To wszystko!

W tym samouczku próbowałem omówić podstawowe kroki, aby utworzyć interfejs API REST dla aplikacji Pythona z frameworkiem sieciowym Bottle.

Możesz pogłębić swoją wiedzę na temat tej małej, ale potężnej struktury, odwiedzając jej samouczek i dokumentację referencyjną interfejsu API.

Powiązane: Budowanie interfejsu API REST Node.js/TypeScript, część 1: Express.js