Статьи

Веб-приложения Python: основы WSGI

Основы WSGI

Под Django , Flask , Bottle и любой другой веб-инфраструктурой Python находится интерфейс шлюза веб-сервера или, для краткости, WSGI. WSGI для Python — это то же, что сервлеты для Java — общая спецификация для веб-серверов, которая позволяет различным веб-серверам и средам приложений взаимодействовать на основе общего API. Однако, как и в большинстве случаев, версия Python значительно проще.

WSGI определен в PEP 3333 , который я рекомендую вам прочитать в качестве справочного материала, если вы хотите получить больше информации после этого краткого вступления.

Эта статья познакомит вас со спецификацией WSGI с точки зрения разработчика приложений и покажет, как работать напрямую с WSGI для создания приложений (если вы того пожелаете).

Ваше первое приложение WSGI

Вот самое простое веб-приложение на Python:

def app(environ, start_fn): start_fn('200 OK', [('Content-Type', 'text/plain')]) return ["Hello World!\n"] 

Это оно! Весь файл. Назовите его app.py и запустите его на любом WSGI-совместимом сервере, и вы получите ответ Hello World со статусом 200. Вы можете использовать Gunicorn для этого; просто установите его через pip ( pip install gunicorn ) и запустите его с gunicorn app:app . Эта команда указывает gunicorn получить вызываемый WSGI из переменной приложения в модуле приложения.

Прямо сейчас вы должны быть очень взволнованы. Всего 3 строки для работающего приложения? Это должна быть какая-то запись (за исключением PHP, потому что mod_php обманывает). Бьюсь об заклад, вы просто рвется узнать больше

Итак, каковы основные части приложения WSGI?

  • Приложение WSGI может вызываться на Python, например, функцией, классом или экземпляром класса с помощью метода __call__
  • Вызываемое приложение должно принимать два аргумента: environ , которое является Python-диктом, содержащим данные запроса, и start_fn , само вызываемое.
  • Приложение должно вызвать start_fn с двумя аргументами: кодом состояния (в виде строки) и списком заголовков, выраженным в виде двух кортежей.
  • Приложение возвращает итеративное содержимое, содержащее байты в теле ответа в виде удобных, потоковых фрагментов — в данном случае это список строк, содержащих просто "Hello, World!" , (Если app является классом, это можно сделать с __iter__ метода __iter__ .)

В качестве примера, следующие два примера эквивалентны первому:

 class app(object): def __init__(self, environ, start_fn): self.environ = environ self.start_fn = start_fn def __iter__(self): self.start_fn('200 OK', [('Content-Type', 'text/plain')]) yield "Hello World!\n" 
 class Application(object): def __call__(self, environ, start_fn): start_fn('200 OK', [('Content-Type', 'text/plain')]) yield "Hello World!\n" app = Application() 

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

Jazzing It Up

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

Например, скажем, мы хотим изучить содержимое environ . Мы можем легко создать промежуточное программное обеспечение для этого, как в этом примере:

 import pprint def handler(environ, start_fn): start_fn('200 OK', [('Content-Type', 'text/plain')]) return ["Hello World!\n"] def log_environ(handler): def _inner(environ, start_fn): pprint.pprint(environ) return handler(environ, start_fn) return _inner app = log_environ(handler) 

Здесь log_environ — это функция, которая возвращает функцию, которая красиво печатает аргумент environ прежде чем отложить на исходный обратный вызов.

Преимущество написания промежуточного программного обеспечения таким образом заключается в том, что промежуточное программное обеспечение и обработчик не должны знать или заботиться друг о друге. Например, вы можете легко log_environ к приложению Flask , поскольку приложения Flask являются приложениями WSGI.

Несколько других полезных идей промежуточного программного обеспечения:

 import pprint def handle_error(handler): def _inner(environ, start_fn): try: return handler(environ, start_fn) except Exception as e: print e # Log error start_fn('500 Server Error', [('Content-Type', 'text/plain')]) return ['500 Server Error'] return _inner def wrap_query_params(handler): def _inner(environ, start_fn): qs = environ.get('QUERY_STRING') environ['QUERY_PARAMS'] = urlparse.parse_qs(qs) return handler(environ, start_fn) return _inner 

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

 # Applied from bottom to top on the way in, then top to bottom on the way out MIDDLEWARES = [wrap_query_params, log_environ, handle_error] app = reduce(lambda h, m: m(h), MIDDLEWARES, handler) 

Вы также можете написать промежуточное программное обеспечение, которое изменяет ответ, используя аргумент start_fn . Вот промежуточное программное обеспечение, которое обращает вывод, если заголовок Content-Type является text/plain :

 def reverser(handler): # A reverse function rev = lambda it: it[::-1] def _inner(environ, start_fn): do_reverse = [] # Must be a reference type such as a list # Override start_fn to check the content type and set a flag def start_reverser(status, headers): for name, value in headers: if (name.lower() == 'content-type' and value.lower() == 'text/plain'): do_reverse.append(True) break # Remember to call `start_fn` start_fn(status, headers) response = handler(environ, start_reverser) try: if do_reverse: return list(rev(map(rev, response))) return response finally: if hasattr(response, 'close'): response.close() return _inner 

Это немного start_fn благодаря разделению start_fn и ответа, но все еще отлично работоспособно.

Также обратите внимание, что, чтобы строго соответствовать спецификации WSGI, мы должны проверить наличие в ответе метода close и вызвать его, если он есть. Устаревшие приложения WSGI также могут возвращать функцию записи вместо итерируемой при вызове handler ; если вы хотите, чтобы ваше промежуточное ПО поддерживало более старые приложения, вам, возможно, придется разобраться с этим делом.

Как только вы немного поиграете с сырым WSGI, вы начнете понимать, почему в Python буквально десятки веб-фреймворков. WSGI упрощает создание чего-либо, начиная с нуля. Например, вы можете рассмотреть проблему маршрутизации:

 routes = { '/': home_handler, '/about': about_handler, } class Application(object): def __init__(self, routes): self.routes = routes def not_found(self, environ, start_fn): start_fn('404 Not Found', [('Content-Type', 'text/plain')]) return ['404 Not Found'] def __call__(self, environ, start_fn): handler = self.routes.get(environ.get('PATH_INFO')) or self.not_found return handler(environ, start_fn) 

Работать с WSGI напрямую можно, если вам нравится гибкость сборки библиотек поверх

  • Библиотеки шаблонов: просто вставьте любую библиотеку шаблонов, которая вам нравится (например, Jinja2 , Pystashe ) и верните обработанный шаблон из вашего обработчика!
  • Помогите вашей маршрутизации с такой библиотекой, как Routes или, возможно, Werkzeug . На самом деле, посмотрите на Werkzeug, если вы хотите использовать очень небольшую абстракцию над WSGI.
  • Используйте любые базы данных / библиотеки миграции, как если бы вы использовали Flask или аналогичные.

Конечно, для неспециализированных приложений вы, вероятно, по-прежнему захотите использовать каркас только для правильной обработки крайних случаев и так далее.

Но как насчет серверов?

Существует множество способов обслуживания приложений WSGI. Мы уже говорили о Gunicorn , который является достойным вариантом. uWSGI — еще один отличный вариант. Просто убедитесь, что вы настроили что-то вроде nginx перед ними для обслуживания статических ресурсов, и у вас должна быть надежная отправная точка.

И это все, что нужно сделать!