Python: Веб-разработка без фреймворков (часть 3)

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

До сих пор в примерах мы почти не занимались собственно ответами, перекладывая эту задачу на какие-то уже существующие приложения. Как уже было упомянуто в описании стандарта WSGI, ответы можно генерировать по частям, однако, как правило, сначала приложение выполняет все нужные вычисления, затем подставляет результаты в шаблон и возвращает полученный документ. Для такой модели работы с ответами хорошо подходит класс webob.Response. Впрочем, он также имеет поддержку последовательной генерации (см. app_iter), просто о ней мы говорить не будем.

webob.Response

Первое, что нужно понять, это то, что экземпляры этого класса являются WSGI приложениями. Создав и наполнив такой экземпляр данными мы получаем WSGI приложение, которое можно использовать единожды или многократно, по желанию. Давайте разберем его использование на примере (внимание, используется декоратор из прошлой статьи):

from webob import Response

@webob_wrap
def hello_app(req):
    if 'name' in req.params:
        if req.charset is None:
            req.charset = 'UTF-8'
            print req.user_agent, 'sucks'
        return Response(unicode_body=u'Hello, %(name)s!' % req.params)
    else:
        return form_app

form_app = Response('<form method=POST><input name=name><input type=submit>')

Хотелось бы сказать пару теплых слов о поддержке браузерами Unicode в запросах: ни IE, ни столь горячо любимый всеми Firefox не способны указать в какой кодировке были отправлены данные формы (если ваш браузер с этой задачей справляется, напишите, пожалуйста, в комментариях). Поэтому, чтобы получать данные формы как положено, в Unicode, нам пришлось добавить несколько строк, которые устанавливают кодировку запроса по умолчанию в UTF-8. Место этих строк, конечно, в нашем незабвенном декораторе, поэтому давайте туда их и перенесем. В результате тело hello_app укоротилось до четырех строк:

@webob_wrap
def hello_app(req):
    if 'name' in req.params:
        return Response(unicode_body=u'Hello, %(name)s!' % req.params)
    else:
        return form_app

Как видно из примера, ответ можно и даже следует создавать из Unicode строк — HTTP ответ будет сгенерирован корректно. Также видно, что экземпляры Response «многоразовые» (см. выше: form_app).

Постепенное наполнение ответа данными

Response не обязательно наполнять данными при инициализации. Можно делать это постепенно, например вот так:

@webob_wrap
def hello_app_cookie(req):
    r = Response(charset='UTF-8')
    if 'name' in req.params:
        name = req.params['name']
        r.set_cookie('name', name)
    elif 'name' in req.cookies:
        name = req.cookies['name']
        #r.delete_cookie('name')
    else:
        return form_app
    r.unicode_body = u'Hello, %s!' % name
    return r

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

Совсем несложно контролировать кеширование (.cache_expires) и условные ответы. Делать это самостоятельно весьма трудоемко, и мало какой фреймворк предоставляет для этого средства. Итак, добавив лишь пару строк, мы получаем поддержку Etag / If-None-Match:

    r.conditional_response = True
    r.md5_etag()
    return r

Тут вызов md5_etag() вычисляет Etag как MD5 хеш тела ответа, но при желании etag можно устанавливать самостоятельно. Ничуть не сложнее работать с If-Modified-Since, для этого у ответа устанавливается .last_modified. Таким образом, хоть мы и сгенерировали ответ целиком, он будет передан клиенту только в том случае, если его кеш устарел.

Request.get_response

Как правило, мы получаем экземпляры Request и строим экземпляры Response, но иногда нам понадобится делать всё ровно наоборот, а именно для тестирования и для написания middleware. Создать новый экземпляр Request не имея полного WSGI окружения очень просто:

test_req = Request.blank('/?name=John')

Естественно, можно наполнить запрос данными присваивая значения соответствующим атрибутам. Использовать же такой объект можно вовсе не только с теми функциями и методами которые готовы работать с WebOb. На самом деле мы можем вызвать с его помощью любое WSGI приложение и получить результат в удобном нам виде, а именно Response:

test_response = test_req.get_response(hello_app_cookie)
assert isinstance(test_response, Response)
assert test_response.body == 'Hello, John!'
print test_response

Весьма удобно, что Response.__str__ возвращает строку с соответствующим HTTP ответом:

200 OK
content-type: text/html; charset=UTF-8
Set-Cookie: name="John"; Path=/
Content-Length: 12
ETag: mE3orpck0aUrpsNFPcMzzw

Hello, John!

Как уже упоминалось, такой подход работает для любых WSGI приложений:

>>> from webob import Request
>>> from paste.fileapp import DataApp
>>> dataapp = DataApp('alert("foo")', [('Content-type', 'text/javascript')])
>>> print Request.blank('/').get_response(dataapp)
200 OK
Content-type: text/javascript
Accept-Ranges: bytes
Last-Modified: Sun, 13 Apr 2008 15:40:34 GMT
ETag: 1208101234.52-12
Content-Range: bytes 0-11/12
Content-Length: 12

alert("foo")

Далее

Следующей будет статья про конфигурацию приложений и использование mod_wsgi. Также в этой серии запланированы статьи о написании middleware, с рекомендациями по работе с шаблонами и ORM а также небольшая обзорная статья о популярных фреймворках.

Підписуйтеся на Telegram-канал «DOU #tech», щоб не пропустити нові технічні статті.

👍НравитсяПонравилось0
В избранноеВ избранном0
Подписаться на автора
LinkedIn



Підписуйтесь: Soundcloud | Google Podcast | YouTube


12 комментариев

Подписаться на комментарииОтписаться от комментариев Комментарии могут оставлять только пользователи с подтвержденными аккаунтами.

Продолжение будет, но врядли до нового года.

> не лучше ли тогда писать кодировку страницы в спрятанном поле? Да, эффективность будет примерно одинакова. Где точно будет выигрыш от хака, так это в русском апаче. Или в случае кривого браузера: получил данные в одной кодировке, на лету адаптировал к кодировке другого сайта, и отослал их уже в измененном виде. Раз «ie=» остается нетронутым, то это исказит данные. Видимо этот случай был вами упомянут выше про «ошибкоустойчивость».P.S. Еще раз спасибо за цикл статей. Интересно почитать их продолжение. Кое-какие моменты мне остаются неясны, из-за чего wsgi надстройка вырождается в маленький фреймворк. Само по себе это не очень страшит, но может получится неподдерживаемый монстр — тогда все пропало:). Если говорить конкретно, то итересен переход от концепции wsgi 1.0 к webob Request/Response и наоборот. У меня эти вещи как-то не стыкуются (то что щас есть — сильно криво).

Да, для внешних форм проблема есть, но не лучше ли тогда писать кодировку страницы в спрятанном поле? Вот у гугла ie= (input encoding) именно это и делает. В общем у меня проблема в таком виде не встречалась чтобы было необходимо делать определение, но если таки встретилась, то ваш вариант с известной строкой мне определенно нравится.

Есть проблема с firefox из линейки 2.0.0.x (freebsd). Думаю, что любой браузер исковеркает данные при переходе с одного сервера на другой, если не совпадает кодировка страницы ссылки и обработчика этого URI веб-сервером. Такое встречается при попытке связать проект с «встраиваемыми» пакетами, типа форумов и вики движков, особенно для тех из них, у кого трудности с уникодом. То, что выше вами же было указано (вероятностный характер функции автоопределения кодировки в браузерах), заставило по новому взглянуть на необходимость универсального решения.P.S. подозреваю, что у сhardet (WebOb/contrib/decorators) могут быть ложные срабатывания, особенно на коротких строках, например, при ссылке на вики аббревиатуру.

Смотреть на content-type запроса бесполезно, его всё равно никто не шлет. В webob/contrib/decorators есть detect_charset, сделал на всякий случай. Там же есть поддержка стратегий по типу Гугловской (? ie=) и вашу можно добавить без труда.Хак мне кажется удачный, но есть ли в нем нужда? Вот какой браузер шлет не в той кодировке?

Автоопределение кодировки оказалось неожиданной засадой. Для себя я решил этот вопрос использованием кнопки submit, например — «продолжить», «поиск» и т.д., которая закодируется вместе со всеми данными в одной кодировке. Если кнопки не полагается по статусу для формы, можно воспользоваться «hidden» переменной. На сервере формируется словарь по возможным используемым кодировкам значения этого известного поля — зная, какое значение должно быть в переменной, можно догадаться, какая функция перекодировки использовалась для этого.Не понятно, правда, что должно быть приоритетней — content-type: /; charset=? или, определение по данным хаком. Есть мысли?

@Naota, Про аякс врядли буду писать, т.к. сам с ним почти не работаю, может только по мелочи что-то. Но в статье про мидлварь будет небольшое приложение склеющиваее и пакующее JS скрипты. С кешированием БД тоже вопрос, сам я предпочитаю кешировать уровнем выше, т.е. готовые HTML блоки. Как это делать думаю и так понятно.

Отличные статьи. Жду с нетерпением продолжение. Очень интересует как грамотно реализовать кэширование к БД. Ну и про аякс было бы неплохо что нить интересное:)

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

довольно неплохоинтересно было бы почитать про более экзотические фреймворки типа werkzeug, web2py, cherry, web.py B)

За похвальбу спасибо, стараюсь.С кодировкой дело такое. Когда браузер отправляет форму через POST, то данные отправляются в теле запроса в том же виде как были бы отправлены через адресную строку в случае GET (т.е. field=value& field2=value итд). Для такого представления существует MIME тип (application/x-www-form-urlencoded), а при передаче текстовых данных предусмотрено указание кодировки, т.е. браузер отправляя POST запрос должен бы послать заголовок «Content-type: application/x-www-form-urlencoded; charset=UTF-8», понятно что кодировка может быть и другой, просто браузер должен её указывать. На самом же деле браузеры вообще этого заголовка не шлют. В случае GET как посылать информацию о кодировке уже не совсем понятно. Насколько я могу судить, Firefox посылает запросы в той же кодировке в которой была получена страница с формой. У Оперы, по крайней мере раньше, была опция посылать в родной кодировке для клиентской машины или в UTF-8. Старые IE, я так понимаю, слали чуть ли не как попало, особенно если было включено автоопределение кодировки на странице.Гугл вот, например, задает кодрировку в одном из полей поисковой формы, возможно для необходимой им ошибкоустойчивости приходится определять как будут посланы данные каким-то совсем уж хитрым способом.В общем стоит отдавать все свои страницы в UTF-8 и тогда можно ожидать что отправленные формы также будут в этой кодировке. Такой подход вроде бы нормально работает.

Все-таки определение кодировки запроса можно поподробней? Когда-то, помнится, это настоящим кошмаром было: размещая пост на русском форуме мозиллой под линуксом часто можно было видеть вместо своих слов знаки вопроса, или koi8 показанный символами cp1251. Сейчас это стало проще, или обычно угадывают по частотам символов? Самый лучший цикл статей на ДОУ на сегодняшний день. Тема раскрывается просто и в занимательной форме. Автору — респект.

Подписаться на комментарии