Создание Rest API с помощью Bottle Framework

Опубликовано: 2022-03-11

API-интерфейсы REST стали распространенным способом установления интерфейса между внутренними и внешними веб-интерфейсами, а также между различными веб-сервисами. Простота интерфейса такого типа и повсеместная поддержка протоколов HTTP и HTTPS в различных сетях и средах делают его удобным выбором при рассмотрении вопросов функциональной совместимости.

Bottle — это минималистичный веб-фреймворк Python. Он легкий, быстрый и простой в использовании и хорошо подходит для создания сервисов RESTful. Базовое сравнение, проведенное Андреем Корнацким, поместило его в тройку лучших фреймворков с точки зрения времени отклика и пропускной способности (запросов в секунду). В моих собственных тестах на виртуальных серверах, доступных в DigitalOcean, я обнаружил, что комбинация серверного стека uWSGI и Bottle может обеспечить накладные расходы всего 140 мкс на запрос.

В этой статье я расскажу, как создать службу RESTful API с помощью Bottle.

Bottle: быстрый и легкий веб-фреймворк Python

Установка и настройка

Каркас Bottle достигает своих впечатляющих характеристик отчасти благодаря легкому весу. Фактически вся библиотека распространяется в виде однофайлового модуля. Это означает, что он не так сильно удерживает вас за руку, как другие фреймворки, но он также более гибкий и может быть адаптирован к множеству различных технологических стеков. Таким образом, Bottle лучше всего подходит для проектов, в которых производительность и настраиваемость имеют первостепенное значение, а экономия времени при использовании более тяжелых фреймворков не имеет большого значения.

Гибкость Bottle делает подробное описание настройки платформы немного бесполезным, поскольку оно может не отражать ваш собственный стек. Тем не менее, краткий обзор опций и где узнать больше о том, как их настроить, уместен здесь:

Установка

Установить Bottle так же просто, как установить любой другой пакет Python. Ваши варианты:

  • Установите в вашей системе с помощью системного менеджера пакетов. Debian Jessie (текущая стабильная версия) упаковывает версию 0.12 как python-bottle .
  • Установите в своей системе с помощью индекса пакетов Python с помощью pip install bottle .
  • Установить в виртуальной среде (рекомендуется).

Чтобы установить Bottle в виртуальной среде, вам понадобятся инструменты virtualenv и pip . Чтобы установить их, обратитесь к документации по virtualenv и pip, хотя они, вероятно, уже есть в вашей системе.

В Bash создайте среду с Python 3:

 $ virtualenv -p `which python3` env

Подавление параметра -p `which python3` приведет к установке интерпретатора Python по умолчанию, присутствующего в системе — обычно Python 2.7. Поддерживается Python 2.7, но в этом руководстве предполагается Python 3.4.

Теперь активируйте окружение и установите Bottle:

 $ . env/bin/activate $ pip install bottle

Вот и все. Бутылка установлена ​​и готова к использованию. Если вы не знакомы с virtualenv или pip , их документация на высшем уровне. Посмотри! Они того стоят.

Сервер

Bottle соответствует стандартному интерфейсу шлюза веб-сервера Python (WSGI), что означает, что его можно использовать с любым сервером, совместимым с WSGI. Сюда входят uWSGI, Tornado, Gunicorn, Apache, Amazon Beanstalk, Google App Engine и другие.

Правильный способ его настройки немного различается в зависимости от среды. Bottle предоставляет объект, соответствующий интерфейсу WSGI, и сервер должен быть настроен для взаимодействия с этим объектом.

Чтобы узнать больше о том, как настроить сервер, обратитесь к документации сервера и документации Bottle здесь.

База данных

Bottle не зависит от базы данных, и ему все равно, откуда берутся данные. Если вы хотите использовать базу данных в своем приложении, в индексе пакетов Python есть несколько интересных опций, таких как SQLAlchemy, PyMongo, MongoEngine, CouchDB и Boto для DynamoDB. Вам нужен только соответствующий адаптер, чтобы заставить его работать с базой данных по вашему выбору.

Основы каркаса бутылки

Теперь давайте посмотрим, как сделать простое приложение в Bottle. Для примеров кода я предполагаю, что Python >= 3.4. Однако большая часть того, что я здесь напишу, будет работать и на Python 2.7.

Базовое приложение в Bottle выглядит так:

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

Когда я говорю базовый, я имею в виду, что эта программа даже не говорит вам «Hello World». (Когда вы в последний раз обращались к интерфейсу REST, который отвечал «Hello World?») Все HTTP-запросы к 127.0.0.1:8000 получат статус ответа 404 Not Found.

Приложения в бутылке

В Bottle может быть создано несколько экземпляров приложений, но для удобства первый экземпляр создается для вас; это приложение по умолчанию. Bottle хранит эти экземпляры в стеке внутри модуля. Всякий раз, когда вы что-то делаете с Bottle (например, запускаете приложение или прикрепляете маршрут) и не указываете, о каком приложении вы говорите, это относится к приложению по умолчанию. На самом деле, строка app = application = bottle.default_app() даже не обязательно должна существовать в этом базовом приложении, но она есть, чтобы мы могли легко вызывать приложение по умолчанию с помощью Gunicorn, uWSGI или какого-либо универсального сервера WSGI.

Возможность использования нескольких приложений поначалу может показаться запутанной, но они добавляют гибкости Bottle. Для различных модулей вашего приложения вы можете создавать специализированные приложения Bottle, создавая экземпляры других классов Bottle и настраивая их с различными конфигурациями по мере необходимости. Доступ к этим различным приложениям можно было получить по разным URL-адресам через URL-маршрутизатор Bottle. Мы не будем углубляться в это в этом уроке, но вам предлагается ознакомиться с документацией по Bottle здесь и здесь.

Вызов сервера

Последняя строка скрипта запускает Bottle, используя указанный сервер. Если сервер не указан, как в данном случае, сервером по умолчанию является встроенный в Python эталонный сервер WSGI, который подходит только для целей разработки. Другой сервер можно использовать следующим образом:

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

Это синтаксический сахар, который позволяет запустить приложение, запустив этот скрипт. Например, если этот файл называется main.py , вы можете просто запустить python main.py , чтобы запустить приложение. Bottle содержит довольно обширный список серверных адаптеров, которые можно использовать таким образом.

Некоторые серверы WSGI не имеют адаптеров Bottle. Их можно запустить с помощью собственных команд запуска сервера. Например, в uWSGI все, что вам нужно сделать, это вызвать uwsgi следующим образом:

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

Примечание о файловой структуре

Bottle оставляет файловую структуру вашего приложения полностью на ваше усмотрение. Я обнаружил, что мои политики файловой структуры развиваются от проекта к проекту, но, как правило, основаны на философии MVC.

Создание вашего REST API

Конечно, никому не нужен сервер, который возвращает только 404 для каждого запрошенного URI. Я обещал вам, что мы создадим REST API, так что давайте сделаем это.

Предположим, вы хотите создать интерфейс, который манипулирует набором имен. В реальном приложении вы, вероятно, использовали бы для этого базу данных, но в этом примере мы просто будем использовать структуру данных set в памяти.

Скелет нашего API может выглядеть так. Вы можете разместить этот код в любом месте проекта, но я бы рекомендовал отдельный файл API, такой как 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

Маршрутизация

Как мы видим, маршрутизация в Bottle выполняется с помощью декораторов. Импортированные декораторы post , get , put и delete обработчики регистрации для этих четырех действий. Понимание того, как эти работы можно разбить следующим образом:

  • Все вышеперечисленные декораторы являются ярлыком для декораторов маршрутизации default_app . Например, декоратор @get() применяет к bottle.default_app().get() .
  • Все методы маршрутизации в default_app являются ярлыками для route() . Таким образом default_app().get('/') эквивалентно default_app().route(method='GET', '/') .

Таким образом @get('/') совпадает с @route(method='GET', '/') , который совпадает с @bottle.default_app().route(method='GET', '/') , и они могут использоваться взаимозаменяемо.

Одна полезная вещь в декораторе @route заключается в том, что если вы хотите, например, использовать один и тот же обработчик для обработки как обновлений, так и удалений объектов, вы можете просто передать список методов, которые он обрабатывает следующим образом:

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

Хорошо, тогда давайте реализуем некоторые из этих обработчиков.

Создайте свой идеальный REST API с помощью Bottle Framework.

RESTful API — основа современной веб-разработки. Подайте своим клиентам API мощную смесь с серверной частью Bottle.
Твитнуть

ПОСТ: Создание ресурса

Наш обработчик POST может выглядеть так:

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

Ну, это довольно много. Давайте рассмотрим эти шаги по частям.

Разбор тела

Этот API требует, чтобы пользователь отправил строку JSON в тело с атрибутом с именем «имя».

Объект request , импортированный ранее из bottle всегда указывает на текущий запрос и содержит все данные запроса. Его атрибут body содержит поток байтов тела запроса, доступ к которому может получить любая функция, способная читать объект потока (например, чтение файла).

Метод request.json() проверяет заголовки запроса на тип контента «application/json» и анализирует тело, если оно правильное. Если Bottle обнаруживает искаженное тело (например, пустое или с неправильным типом содержимого), этот метод возвращает None , и поэтому мы ValueError . Если синтаксический анализатор JSON обнаруживает искаженное содержимое JSON; он вызывает исключение, которое мы перехватываем и повторно вызываем, опять же как ValueError .

Разбор и проверка объектов

Если ошибок нет, мы преобразовали тело запроса в объект Python, на который ссылается переменная data . Если мы получили словарь с ключом «name», мы сможем получить к нему доступ через data['name'] . Если мы получили словарь без этого ключа, попытка доступа к нему приведет нас к исключению KeyError . Если мы получили что-то кроме словаря, мы получим исключение TypeError . Если возникает какая-либо из этих ошибок, мы снова вызываем ее как ValueError , указывая на неверный ввод.

Чтобы проверить, имеет ли ключ имени правильный формат, мы должны протестировать его против маски регулярного выражения, такой как маска namepattern , которую мы создали здесь. Если name ключа не является строкой, namepattern.match() вызовет TypeError , а если оно не совпадает, вернет None .

С маской в ​​этом примере имя должно быть буквенно-цифровым ASCII без пробелов от 1 до 64 символов. Это простая проверка, и она не проверяет, например, объект с мусорными данными. Более сложная и полная проверка может быть достигнута с помощью таких инструментов, как FormEncode.

Проверка на существование

Последний тест перед выполнением запроса — существует ли данное имя в наборе. В более структурированном приложении этот тест, вероятно, должен выполняться выделенным модулем и сигнализироваться нашему API через специализированное исключение, но, поскольку мы напрямую манипулируем набором, мы должны сделать это здесь.

Мы сигнализируем о существовании имени, вызывая KeyError .

Ответы на ошибки

Точно так же, как объект запроса содержит все данные запроса, объект ответа делает то же самое для данных ответа. Есть два способа установить статус ответа:

 response.status = 400

а также:

 response.status = '400 Bad Request'

Для нашего примера мы выбрали более простую форму, но вторая форма может использоваться для указания текстового описания ошибки. Внутри Bottle разделит вторую строку и установит числовой код соответствующим образом.

Успешный ответ

Если все шаги выполнены успешно, мы выполняем запрос, добавляя имя в набор _names , устанавливая заголовок ответа Content-Type и возвращая ответ. Любая строка, возвращаемая функцией, будет рассматриваться как тело ответа 200 Success , поэтому мы просто генерируем его с помощью json.dumps .

ПОЛУЧИТЬ: Список ресурсов

Переходя от создания имени, мы реализуем обработчик списка имен:

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

Перечислять имена было намного проще, не так ли? По сравнению с созданием имени здесь нечего делать. Просто установите несколько заголовков ответа и верните JSON-представление всех имен, и все готово.

PUT: обновление ресурсов

Теперь давайте посмотрим, как реализовать метод обновления. Он не сильно отличается от метода create, но мы используем этот пример для введения параметров 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})

Схема тела для действия обновления такая же, как и для действия создания, но теперь у нас также есть новый параметр oldname в URI, определенный маршрутом @put('/names/<oldname>') .

Параметры URI

Как видите, нотация Bottle для параметров URI очень проста. Вы можете создавать URI с любым количеством параметров. Bottle автоматически извлекает их из URI и передает обработчику запроса:

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

Используя декораторы каскадных маршрутов, вы можете создавать URI с дополнительными параметрами:

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

Кроме того, Bottle позволяет использовать следующие фильтры маршрутизации в URI:

  • int

Соответствует только параметрам, которые могут быть преобразованы в int , и передает преобразованное значение обработчику:

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

То же, что и int , но со значениями с плавающей запятой:

 @get('/<param:float>') def handler(param): pass
  • re (регулярные выражения)

Соответствует только параметрам, которые соответствуют заданному регулярному выражению:

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

Сопоставляет подсегменты пути URI гибким способом:

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

Спички:

  • /x/id , передавая x в качестве param .
  • /x/y/id , передавая x/y в качестве param .

УДАЛИТЬ: удаление ресурса

Как и метод GET, метод DELETE приносит нам мало новостей. Просто обратите внимание, что возврат None без установки статуса возвращает ответ с пустым телом и кодом состояния 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

Последний шаг: активация API

Предположим, мы сохранили API имен как api/names.py , теперь мы можем включить эти маршруты в основном файле приложения 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)

Обратите внимание, что мы импортировали только модуль names . Поскольку мы украсили все методы их URI, прикрепленными к приложению по умолчанию, дальнейшая настройка не требуется. Наши методы уже на месте, готовы к доступу.

Ничто так не радует интерфейс, как хорошо сделанный REST API. Работает как шарм!

Вы можете использовать такие инструменты, как Curl или Postman, чтобы использовать API и тестировать его вручную. (Если вы используете Curl, вы можете использовать средство форматирования JSON, чтобы ответ выглядел менее загроможденным.)

Бонус: совместное использование ресурсов между источниками (CORS)

Одной из распространенных причин для создания REST API является взаимодействие с внешним интерфейсом JavaScript через AJAX. Для некоторых приложений эти запросы должны быть разрешены из любого домена, а не только из домашнего домена вашего API. По умолчанию большинство браузеров запрещают такое поведение, поэтому позвольте мне показать вам, как настроить совместное использование ресурсов между источниками (CORS) в Bottle, чтобы разрешить это:

 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

hook -декоратор позволяет нам вызывать функцию до или после каждого запроса. В нашем случае, чтобы включить CORS, мы должны установить заголовки Access-Control-Allow-Origin , -Allow-Methods и -Allow-Headers для каждого из наших ответов. Они указывают запрашивающему, что мы будем обслуживать указанные запросы.

Кроме того, клиент может сделать HTTP-запрос OPTIONS к серверу, чтобы узнать, действительно ли он может делать запросы другими методами. В этом универсальном примере мы отвечаем на все запросы OPTIONS с кодом состояния 200 и пустым телом.

Чтобы включить это, просто сохраните его и импортируйте из основного модуля.

Заворачивать

Вот и все!

В этом руководстве я попытался описать основные шаги по созданию REST API для приложения Python с веб-платформой Bottle.

Вы можете углубить свои знания об этом небольшом, но мощном фреймворке, посетив его учебник и справочные документы по API.

Связано: Создание REST API Node.js/TypeScript, часть 1: Express.js