WSGI (pep-333)¶
WSGI — стандарт взаимодействия между Python-программой, выполняющейся на стороне сервера, и самим веб-сервером, например Apache.
В Python существует большое количество различного рода веб-фреймворков, тулкитов и библиотек. У каждого из них собственный метод установки и настройки, они не умеют взаимодействовать между собой. Это может стать затруднением для тех, кто только начинает изучать Python, так как, например, выбор определённого фреймворка может ограничить выбор веб-сервера и наоборот.
WSGI предоставляет простой и универсальный интерфейс между большинством веб-серверов и веб-приложениями или фреймворками.
Пример работы WSGI (автор Ян Бикинг)
Application¶
По стандарту, WSGI-приложение должно удовлетворять следующим требованиям:
- должно быть вызываемым (callable) объектом (обычно это функция или метод)
- принимать два параметра:
- словарь переменных окружения (environ)
- обработчик запроса (start_response)
Простейшим примером WSGI-приложения может служить такая функция-генератор:
def simple_app(environ, start_response): """ (dict, callable( status: str, headers: list[(header_name: str, header_value: str)])) -> body: iterable of strings """ status = '200 OK' response_headers = [('Content-type', 'text/plain')] start_response(status, response_headers) return 'Hello world!\n'
или то же самое в виде класса:
class AppClass(object): def __init__(self, environ, start_response): self.environ = environ self.start = start_response def __iter__(self): status = '200 OK' response_headers = [('Content-type', 'text/plain')] self.start(status, response_headers) yield "Hello world!\n"
Server/Gateway¶
Чтобы запустить наше WSGI приложение, нужен WSGI сервер. Он запускает WSGI приложение один раз при каждом HTTP запросе от клиента.
- Сформировать переменные окружения (environment)
- Описать функцию обработчик запроса (start_response)
- Передать их в WSGI приложение
- Результат WSGI сервер отправляет по HTTP клиенту
- а WSGI шлюз приводит к формату клиент-серверного протокола (CGI, FastCGI, SCGI, uWSGI, …) и передает их на Веб-сервер (например выводит в stdout, stderr).
Пример WSGI-шлюза к CGI-серверу.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61
import os import sys def run_with_cgi(application): environ = dict(os.environ.items()) environ['wsgi.input'] = sys.stdin environ['wsgi.errors'] = sys.stderr environ['wsgi.version'] = (1, 0) environ['wsgi.multithread'] = False environ['wsgi.multiprocess'] = True environ['wsgi.run_once'] = True if environ.get('HTTPS', 'off') in ('on', '1'): environ['wsgi.url_scheme'] = 'https' else: environ['wsgi.url_scheme'] = 'http' headers_set = [] headers_sent = [] def write(data): if not headers_set: raise AssertionError("write() before start_response()") elif not headers_sent: # Before the first output, send the stored headers status, response_headers = headers_sent[:] = headers_set sys.stdout.write('Status: %s\r\n' % status) for header in response_headers: sys.stdout.write('%s: %s\r\n' % header) sys.stdout.write('\r\n') sys.stdout.write(data) sys.stdout.flush() def start_response(status, response_headers, exc_info=None): if exc_info: try: if headers_sent: # Re-raise original exception if headers sent raise Exception(exc_info[0], exc_info[1], exc_info[2]) finally: exc_info = None # avoid dangling circular ref elif headers_set: raise AssertionError("Headers already set!") headers_set[:] = [status, response_headers] return write result = application(environ, start_response) try: for data in result: if data: # don't send headers until body appears write(data) if not headers_sent: write('') # send headers now if body was empty finally: if hasattr(result, 'close'): result.close()
Environment¶
- environ это обычно копия переменных окружения ОС os.environ + стандартные CGI переменные. SCRIPT_NAME — содержит имя вызванного скрипта. Например: myapp.py PATH_INFO — путь к файлу /cgi-bin/myapp.py
- также включает в себя дополнительные WSGI-специфичные переменные, наиболее важные из них: wsgi.input — представляет тело (body) HTTP запроса. wsgi.errors — указывает поток куда нужно выводить ошибки. wsgi.url_scheme — это просто «http» или «https».
start_response¶
Функция start_response принимает два обязательных аргумента:
- status — строка содержащая статус HTTP ответа, например 200 OK .
- response_headers — список кортежей, которые содержат заголовки ответа, например [(‘Content-Type’, ‘text/html’), (‘Content-Length’, ’15’) .
def simple_app(environ, start_response): """ (dict, callable( status: str, headers: list[(header_name: str, header_value: str)])) -> body: iterable of strings """ status = '200 OK' response_headers = [('Content-type', 'text/plain')] start_response(status, response_headers) return 'Hello world!\n'
start_response возвращает вызываемый объект, обычно «write». write выводит тело ответа в поток вывода, используется при необычных обстоятельствах.
Обратный вызов write плохо поддерживается серверами и веб-фреймворками, поэтому рекомендуется проектировать свои приложения без его вызова.
Обычно данные возвращаются таким образом:
def application(environ, start_response): start_response(status, headers) return ['content block 1', 'content block 2', 'content block 3']
def application(environ, start_response): write = start_response(status, headers) write('content block 1') return ['content block 2', 'content block 3']
Запуск нашего приложения через WSGI-шлюз к CGI
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
#! /usr/bin/env python # -*- coding: utf-8 -*- # vim:fenc=utf-8 # # Copyright © 2015 uralbash # # Distributed under terms of the MIT license. """ Example from pep-333 """ from cgi_gateway import run_with_cgi def simple_app(environ, start_response): """ (dict, callable( status: str, headers: list[(header_name: str, header_value: str)])) -> body: iterable of strings """ status = '200 OK' response_headers = [('Content-type', 'text/plain')] start_response(status, response_headers) return 'Hello world!\n' class AppClass(object): def __init__(self, environ, start_response): self.environ = environ self.start = start_response def __iter__(self): status = '200 OK' response_headers = [('Content-type', 'text/plain')] self.start(status, response_headers) yield "Hello world!\n" if __name__ == '__main__': run_with_cgi(AppClass)
$ python 1.cgi.app.py Status: 200 OK Content-type: text/plain Hello world!
В исходных кодах к лекциям cgiserver.py делает этот пример доступным по адресу http://localhost:8000/wsgi/1.cgi.app.py
Middleware¶
Помимо приложений и серверов, стандарт дает определение middleware-компонентов, предоставляющих интерфейсы как приложению, так и серверу. То есть для сервера middleware является приложением, а для приложения сервером. Это позволяет составлять «цепочки» WSGI-совместимых middleware.
Middleware могут брать на себя следующие функции (но не ограничиваются этим):
- обработка сессий
- аутентификация/авторизация
- управление URL (маршрутизация запросов)
- балансировка нагрузки
- пост-обработка выходных данных (например, проверка на валидность)
Мы рассмотрим пример приложения, которое считает количество обращений и использует следующие middleware:
Приложение¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
def app(environ, start_response): # Except error if 'error' in environ['PATH_INFO'].lower(): raise Exception('Detect "error" in URL path') # Session session = environ.get('paste.session.factory', lambda: <>)() if 'count' in session: count = session['count'] else: count = 1 session['count'] = count + 1 # Generate response start_response('200 OK', [('Content-Type', 'text/plain')]) return [b'You have been here %d times!\n' % count, ]
Приложение выводит число 1 при первом обращении, записывает его в сессию и при каждом последующем обращении увеличивает число на 1.
Т.к. протокол HTTP не сохраняет предыдущего состояния, то при обновлении страницы число не увеличится. Чтобы это произошло, нужно реализовать механизм сессий.
Обработчик исключений¶
from paste.evalexception.middleware import EvalException app = EvalException(app)
EvalException позволяет нам отлавливать ошибки и выводить их в браузере. Если мы перейдем по адресу http://localhost:8000/Errors_500, наше приложение найдет слово error в пути и искусственно вызовет исключение.
Сессии¶
from paste.session import SessionMiddleware app = SessionMiddleware(app)
SessionMiddleware добавляет cookie клиенту с ключом _SID_ и номером сессии.
Для каждой сессии на сервере в директории /tmp/ (по умолчанию) создается файл с таким же именем.
$ tree /tmp/ /tmp/ |-- 20150313094744-5d2e448000e6312d7c0b8a02ed954d22 `-- 20150313142600-d18ec118fff970ad4fb3628fbf530bc4 1 directory, 2 files
В этот файл записывается значение count для нашей сессии. При каждом обращении клиента SessionMiddleware находит файл с таким же именем как у cookie _SID_ десереализует объекты в нем и присваивает переменной окружения paste.session.factory . Таким образом мы можем хранить состояние сессии и при каждом обновлении будет отдаваться значение, увеличенное на 1.
Сжатие Gzip¶
from paste.gzipper import middleware as GzipMiddleware app = GzipMiddleware(app)
GzipMiddleware сжимает ответ методом gzip
Pony¶
from paste.pony import PonyMiddleware app = PonyMiddleware(app)
Это самое важное расширение в WSGI. Доступно по адресу http://localhost:8000/pony.
Полный пример¶
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
#! /usr/bin/env nix-shell #! nix-shell -i python3 -p python3 python3Packages.paste # -*- coding: utf-8 -*- # vim:fenc=utf-8 # # Copyright © 2015 uralbash # # Distributed under terms of the MIT license. """ 3.http.middleware.py """ from paste.evalexception.middleware import EvalException from paste.gzipper import middleware as GzipMiddleware from paste.pony import PonyMiddleware from paste.session import SessionMiddleware def app(environ, start_response): # Except error if 'error' in environ['PATH_INFO'].lower(): raise Exception('Detect "error" in URL path') # Session session = environ.get('paste.session.factory', lambda: <>)() if 'count' in session: count = session['count'] else: count = 1 session['count'] = count + 1 # Generate response start_response('200 OK', [('Content-Type', 'text/plain')]) return [b'You have been here %d times!\n' % count, ] app = EvalException(app) # go to http://localhost:8000/Errors app = SessionMiddleware(app) app = GzipMiddleware(app) app = PonyMiddleware(app) # go to http://localhost:8000/pony if __name__ == '__main__': from paste import reloader from paste.httpserver import serve reloader.install() serve(app, host='0.0.0.0', port=8000)
Свой middleware¶
1 2 3 4 5 6 7 8 9 10 11 12
class GoogleRefMiddleware(object): def __init__(self, app): self.app = app def __call__(self, environ, start_response): environ['google'] = False if 'HTTP_REFERER' in environ: if environ['HTTP_REFERER'].startswith('http://google.com'): environ['google'] = True return self.app(environ, start_response) app = GoogleRefMiddleware(app)
GoogleRefMiddleware добавляет переменную окружения google , и если бы мы перешли на наш сайт из поиска google.com , тo это значение было бы True .
Кто использует WSGI?¶
- BlueBream
- bobo
- Bottle
- CherryPy
- Django
- Eventlet
- Flask
- Google App Engine’s webapp2
- Gunicorn
- prestans
- mod_wsgi для Apache
- MoinMoin
- netius
- Plone
- Pylons
- Pyramid
- repoze
- restlite
- Tornado
- Trac
- TurboGears
- Uliweb
- webpy
- Falcon
- web2py
- weblayer
- Werkzeug
- Zope
- и многие другие
Аналоги¶
- Rack – Ruby web server interface
- PSGI – Perl Web Server Gateway Interface
- JSGI – JavaScript web server gateway interface
- WAI — Web Application Interface (Haskell)
- Ring — Clojure
© Copyright 2020, Кафедра Интеллектуальных Информационных Технологий ИнФО УрФУ. Created using Sphinx 1.7.6.