См.также
WSGI — стандарт взаимодействия между Python-программой, выполняющейся на стороне сервера, и самим веб-сервером, например Apache.
Идея:
В Python существует большое количество различного рода веб-фреймворков, тулкитов и библиотек. У каждого из них собственный метод установки и настройки, они не умеют взаимодействовать между собой. Это может стать затруднением для тех, кто только начинает изучать Python, так как, например, выбор определённого фреймворка может ограничить выбор веб-сервера и наоборот.
WSGI предоставляет простой и универсальный интерфейс между большинством веб-серверов и веб-приложениями или фреймворками.
По стандарту, WSGI-приложение должно удовлетворять следующим требованиям:
Простейшим примером WSGI-приложения может служить такая функция-генератор:
1 2 3 4 5 6 7 8 9 10 | 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' |
или то же самое в виде класса:
1 2 3 4 5 6 7 8 9 10 | 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" |
Чтобы запустить наше WSGI приложение, нужен WSGI сервер. Он запускает WSGI приложение один раз при каждом HTTP запросе от клиента.
Задачи WSGI сервера:
Пример 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() |
Всегда словарь
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
принимает два обязательных аргумента:
status
- строка содержащая статус HTTP ответа, например 200 OK
.response_headers
- список кортежей, которые содержат заголовки ответа,
например [('Content-Type', 'text/html'), ('Content-Length', '15')
.1 2 3 4 5 6 7 8 9 10 | 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 <root@uralbash.ru>
#
# 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 является приложением, а для приложения сервером. Это позволяет составлять «цепочки» WSGI-совместимых middleware.
Middleware могут брать на себя следующие функции (но не ограничиваются этим):
Мы рассмотрим пример приложения, которое считает количество обращений и использует следующие 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 не сохраняет предыдущего состояния, то при обновлении страницы число не увеличится. Чтобы это произошло, нужно реализовать механизм сессий.
1 2 | from paste.evalexception.middleware import EvalException
app = EvalException(app) |
EvalException
позволяет нам отлавливать ошибки и выводить их в браузере.
Если мы перейдем по адресу http://localhost:8000/Errors_500, наше приложение
найдет слово error в пути и искусственно вызовет исключение.
1 2 | from paste.session import SessionMiddleware
app = SessionMiddleware(app) |
SessionMiddleware
добавляет cookie клиенту с ключом _SID_ и номером
сессии.
Например _SID_=20150313142600-d18ec118fff970ad4fb3628fbf530bc4
Для каждой сессии на сервере в директории /tmp/
(по умолчанию) создается
файл с таким же именем.
$ tree /tmp/
/tmp/
|-- 20150313094744-5d2e448000e6312d7c0b8a02ed954d22
`-- 20150313142600-d18ec118fff970ad4fb3628fbf530bc4
1 directory, 2 files
В этот файл записывается значение count
для нашей сессии. При каждом
обращении клиента SessionMiddleware
находит файл с таким же именем как у
cookie _SID_
десереализует объекты в нем и присваивает переменной окружения
paste.session.factory
. Таким образом мы можем хранить состояние сессии и
при каждом обновлении будет отдаваться значение, увеличенное на 1.
1 2 | from paste.gzipper import middleware as GzipMiddleware
app = GzipMiddleware(app) |
GzipMiddleware
сжимает ответ методом gzip
1 2 | 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 <root@uralbash.ru>
#
# 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) |
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
.