Fork me on GitHub
7/5/2007

WSGI, Paste

Продолжим работать с WSGI. Сегодня поговорим о Paste.

Когда заходит разговор о Paste, возникает проблема определения. Достаточно сложно одним-двумя словами сказать что же такое Paste. Набор WSGI-компонент и middleware... Если человек не особо плотно работает с WSGI, то это ему ничего не скажет. Бен Бангерт в своем выступлении на Google Tech Talks называет Paste "те компоненты, функционал которых обычно ожидают от фреймворка", "набор компонент, слишком мелких для выделения в отдельные пакеты".

Давайте на простых примерах разберемся что такое Paste, что умеет и зачем нужен. Paste состоит из трех частей: собственно Paste, PasteDeploy и PasteScript. Сегодня будем разговаривать о "просто Paste".

Необходимое предисловие

Для демонстрации Paste мы будем использовать простейшее WSGI-приложение, либо небольшие его вариации:

def sample_app(environ, start_response):
    start_response('200 OK', [('Content-type', 'text/html')])
    sorted_keys = sorted(environ.keys())
    content = ['<html><body><h1>TrivialWSGIApp in action</h1>'] + \
             ['<p>Sample WSGI application. Just show your environment.</p><p><ul>'] + \
             ['<li> %s : %s</li>' % (str(k), str(environ[k])) for k in sorted_keys] + \
             ['</ul></p></body></html>']
    return content

Ну и прежде чем приступить, установим Paste (он активно использует возможности setuptools и сам распространяется в виде egg, так что лучше если вы заранее настроите easy_install)

$ easy_install Paste

Сервер

Во-первых, Paste предоставляет сервер для работы с WSGI-приложениями. Естественно, может использоваться как на месте разработчика (чтобы не "тащить" за собой Apache, Lighttpd или Nginx), так и при развертывании (он достаточно быстр).

Что ж, давайте попробуем.

>>> from paste import httpserver

>>> httpserver.serve(sample_app, host='127.0.0.1', port=5000)
serving on http://127.0.0.1:5000

Вуаля...

Сервер Paste сделан на основе BaseHTTPServer и если у вас есть "своя" реализация сервера, наследованная от BaseHTTPServer, то Paste дает возможность более-менее просто сделать свой сервер WSGI-совместимым. Для этого следует воспользоваться WSGIHandlerMixIn.

Диспетчеризация

Каскад

Каскад приложений. Из каскада возвращается первый не-ошибочный ответ.

Например, есть некое приложение, которое будет возвращать ошибку 404, если в пути не будет присутствовать foo.

def foo_app(environ, start_response):
    if 'foo' in environ['PATH_INFO']:
        start_response('200 OK', [('Content-type', 'text/html')])
        content = ['<html><body><h1>Foo app</h1>'] + \
                  ['<p>We\'ve found <code>foo</code> in path</p>']
    else:
        start_response('404 Not found', [('Content-type', 'text/html')])
        content = ['<html><body><h1>404: Not found</h1>'] + \
                  ['<p>Foo not found in path</p>']
    return content

И наше sample_app. Если мы составим такой каскад:

from paste import cascade
cascaded = cascade.Cascade([foo_app, sample_app], catch=(404,500))

То если foo_app вернет 404, то запрос будет обслужен sample_app. По умолчанию Cascade перехватывает именно 404, однако можно указать и другие, например 500. Это позволяет организовывать различного рода "умные" обработчики ошибок.

Префиксы

Если каскад это аналог списка или кортежа, то префиксы - аналог словаря. В зависимости от префикса запрос обслуживается либо одним, либо другим приложением.

from paste import urlmap

mapping = urlmap.URLMap()
mapping['/foo'] = foo_app
mapping['/bar'] = sample_app

Теперь URLы, начинающиеся с 'foo' будет обслуживать foo_app, а начинающиеся с 'bar' - приложение sample_app Заметьте одну особенность: если вы пойдете по URL http://127.0.0.1:5000/foo, то приложение foo_app ругнется, что foo нет в пути. И действительно, Paste "вырезает" префикс из пути (PATH_INFO). И это правильно.

Префиксы полезны, когда вы в один сайт вставляете несколько WSGI-приложений. Например, форум и блог.

Обработчики ошибок

Если не нужно "хитрых" обработчиков, а нужно просто делать редиректы на страницы с ошибками, то в этом вам поможет errordocument.

from paste import fileapp, urlmap, errordocument

def err404_app(environ, start_response):
    start_response('200 OK', [('Content-type', 'text/html')])
    return ["<html><body><h1>Err 404: Object not found in %s</h1></body></html>" % environ['paste.recursive.old_path_info']]

mapping = urlmap.URLMap()
mapping['/foo'] = foo_app
mapping['/err404'] = err404_app

error_handled = errordocument.forward(mapping, codes={404:'/err404'})

Отметим, что обработчик должен возвращать ответ 200 OK, а иначе будет зацикливание обработчиков. Стоит обратить внимание, что в переменных окружения фиксируются операции, проводимые paste. В частности, можно получить значение переменной окружения PATH_INFO до редиректа. Упомянем еще одну особенность: при запросе /foo, PATH_INFO будет пустым, поскольку paste имитирует корень для приложения foo_app. Это сделано затем, чтобы приложение обрабатывало только свой "кусок" URL'а.

Инструменты

Запросы и ответы (request/response)

Удобные обертки для запросов и ответов.

from paste import wsgiwrappers

def advanced_app(environ, start_response):
    request = wsgiwrappers.WSGIRequest(environ)

    response = wsgiwrappers.WSGIResponse("<html><body><h1>Advanced WSGI app in action</h1>")
    response.write("<p>Your preferred languages are: %s</p>" % request.languages)
    response.write("<p>Params of your query are: %s</p>" % request.params)
    response.write("</html></body>")

    return response(environ, start_response)

Код говорит сам за себя. С такими объектами гораздо удобней работать, чем с "голым WSGI".

Тестирование

Paste предоставляет простой интерфейс для тестирования WSGI-приложений. Используя paste.fixture, делаем тест-обертку для приложения, и работаем уже с этой оберткой:

from paste import fixture
import unittest

def testing_app(environ, start_response):
    start_response('200 OK', [('Content-Type', 'text/plain'),
                              ('X-Powered-By', 'Python Paste')])
    return ["Paste is a collection of useful tools for WSGI"]

class WSGIAppTestCase(unittest.TestCase):
    def setUp(self):
        self.test_app = fixture.TestApp(testing_app)

    def test_headers(self):
        response = self.test_app.get('/path/to/url')
        self.assertEquals(response.header('X-Powered-By'), 'Python Paste')

    def test_content(self):
        response = self.test_app.get('/path/to/url')
        # similar to response.mustcontain('WSGI'), but more pythonic
        self.assert_('WSGI' in response)

Тут же упомяну и paste.lint.middleware - middleware, проверяющая корректность реализации WSGI.

Отладка

Paste дает возможность вылавливать ошибки самыми различными способами

paste.exceptions

Отлов ошибок и показ traceback либо в браузере (при отладке), либо письмом.

from paste.exceptions import errormiddleware

def erroneous_app(environ, start_response):
    if environ['PATH_INFO'] == '/error':
        raise Exception("Some error occured here")

    start_response('200 OK', [('Content-Type', 'text/html')])
    return ['<html><body><h1>Erroreous application</h1>'] + \
           ['<p>Go to <a href="/error">/error</a> to see exception</p>'] + \
           ['</body></html>']

app = errormiddleware.ErrorMiddleware(erroneous_app, debug=True)

paste.cgitb_catcher

Любители фиолетовых страниц ошибок в стиле cgitb могут получать их для WSGI-приложений средствами paste.cgitb_catcher.

from paste import cgitb_catcher
app = cgitb_catcher.CgitbMiddleware(erroneous_app, display=True)

paste.evalexception

Результат работы этого middleware сильно похож на paste.exceptions, но с одним большим плюсом: в нем есть интерактивная Python-консоль, так что вы можете более подробно изучить причины ошибки.

from paste import evalexception
app = evalexception.EvalException(erroneous_app)

Профилирование

При помощи paste.debug.profile мы можем "на лету" отпрофилировать приложение и сразу же посмотреть результаты в веб-браузере.

from paste.debug import profile
app = profile.ProfileMiddleware(sample_app, None)

Показ print'ов на странице

Интересный middleware - "отлавливает" print'ы и показывает их на странице. Примерно так:

from paste.debug import prints

def sample_app_prints(environ, start_response):
    print "start response with 200 OK status"
    start_response('200 OK', [('Content-type', 'text/html')])
    print "retrieve environ keys"
    sorted_keys = environ.keys()
    print "sort keys"
    sorted_keys.sort()
    print "construct content"

    content = ['<html><body><h1>Trivial WSGI app in action</h1>'] + \
              ['<p>Sample WSGI application. Just show your environment.</p><p><ul>'] + \
              ['<li> %s : %s</li>' % (str(k), str(environ[k])) for k in sorted_keys] + \
              ['</ul></p></body></html>']
    print "return content"

    return content

app = prints.PrintDebugMiddleware(sample_app_prints)

Еще...

В Paste помимо всего вышеописанного есть:

  • Работа с WSGI-переменными из environ в paste.request
  • Представление cgi-приложений как WSGI в paste.cgiapp
  • Представление любого HTTP-приложения как WSGI в paste.proxy
  • Обработка исключений (напр. HTTPNotFound) в правильные HTTP-ответы в paste.httpexceptions
  • Управление сессиями в paste.session и paste.flup_session
  • Сжатие на лету трафика средствами paste.gzipper
  • Журнал обращений в стиле Apache в paste.translogger
  • Управление глобальными переменными в paste.registry, thread-safe
  • Различного рода аутентификации (в том числе и с поддержкой OpenID) в paste.auth
  • Монитор изменений (перезагружает сервер при изменении контролируемого кода) в paste.reloader
  • ... и кое-что еще

Итог

Paste - это "кирпичики", из которых можно строить свое приложение или фреймворк (Pylons, RhubarbTart, CleverHarold поступают именно так). Ряд интересных middleware (то же самое профилирование) вы можете использовать не только с этими фреймворками (где все компоненты - WSGI-совместимы) или с собственным WSGI-приложенем, но и с Django, где WSGI - это внешний интерфейс.

Полный код всех примеров, как всегда, можно найти на code.google.com.

Комментарии

Все статьи