python

От Python скрипта до WSGI приложения

  • пятница, 24 апреля 2015 г. в 02:10:57
http://habrahabr.ru/post/256481/

Появилась задача написать веб интерфейс управления устройством. Управлять устройством будет Raspberry Pi. Логика управления написана python, соответственно и интерфейс хотелось бы на python. Хочу поделится своим опытом.

  • 1. lighttpd mod_cgi и простой скрипт
  • 2. web.py на порту 8080
  • 3. WCGI интерфейс
  • 4. Простой сервер WSGI
  • 5. WSGI с использованием wsgiref
  • 6. WSGI c помощью flup
  • 7. web.py приложение с использованием flup
  • 8. Немного особенностей


1. Для решения задачи «в лоб» был поднят lighttpd c mod_cgi:

sudo apt-get install lighttpd
sudo nano /etc/lighttpd/lighttpd.conf

Отрывок lighttpd.conf:

#mod_cgi shoud be on
server.modules = (
    "mod_access",
    "mod_alias",
    "mod_compress",
    "mod_redirect",
    "mod_cgi",
    "mod_rewrite",
)
#rule enables cgi script
cgi.assign = (".py" => "/usr/bin/python")

/var/www/index.py:

print "Content-Type: text/html\n\n"
print "Hello World!"

теперь localhost/index.py отвечал бодрым «Hello World!»

Когда lighttpd встречает файл с расширением .py передает его на выполнение python-у и его результатом отвечает на запрос. Грубо говоря перенаправляет stdout.
После некоторых попыток написания интерфейса «с нуля», был рожден HtmlGenerator, который позволил не перегружать код html-тегами, весьма упростил, но все таки не решил проблемы в комплексе.

2. Решено было поэкспериментировать с веб фреймворками.
Под руку попался wep.py, простенький и маловесный.
code.py:

#! /usr/bin/python
#
import web
urls = ( '/', 'index',)

class index:
    def GET(self):
        return "Hello, world!"

if __name__ == "__main__":
    web.application(urls, globals()).run()

Минимальный код и на порту 8080 висит наше веб приложение
Казалось бы пробросить алиас на порт 8080, организовать авто запуск скрипта и все готово.
Да но нет, эксперименты на слабеньком компьютере показали что присутствие нашего скрипта заставляет машинку изрядно «дуться». Кроме того есть lighttpd с mod_cgi.

Как же связать простой скрипт и веб приложение.

3. Согласно описанию WSGI, для его реализации необходим интерфейс такого вида
#! /usr/bin/python
#
def myapp(environ, start_response):
    status = '200 OK'
    response_headers = [('Content-type','text/plain')]
    start_response(status, response_headers)
    return ['Hello World!\n']

Ничего военного, но и что-то не работает, чего-то не хватает.
Момент который был неясным после прочтения вики и других статей, что же все таки запустит наш интерфейс.

4. Для запуска WSGI приложения нужен сервер. Пример скрипта который может выступать в роли простого сервера WSGI:
wsgi.py:
#! /usr/bin/python
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:
            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:
                    raise exc_info[0], exc_info[1], exc_info[2]
            finally:
                exc_info = None
        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:
                write(data)
        if not headers_sent:
            write('')
    finally:
        if hasattr(result, 'close'):
            result.close()

Теперь добавив к нашему интерфейсу его запуск получим скрипт который ответит уже на нашем lighttpd или apache, по адресу localhost/app.py
/var/www/app.py:
#! /usr/bin/python
include wsgi

def myapp(environ, start_response):
    status = '200 OK'
    response_headers = [('Content-type','text/plain')]
    start_response(status, response_headers)
    return ['Hello World!\n']

if __name__ == '__main__':
    wsgi.run_with_cgi(myapp)


5. Для python 2.7 доступен модуль wsgiref который может реализовать WSGI сервер
#! /usr/bin/python
import wsgiref.handlers

def myapp(environ, start_response):
    status = '200 OK'
    response_headers = [('Content-type','text/plain')]
    start_response(status, response_headers)
    return ['Hello World!\n']

if __name__ == '__main__':
    wsgiref.handlers.CGIHandler().run(myapp)


6. Реализация WSGI c помощью flup:
установим flup
sudo apt-get install python-flup

#! /usr/bin/python
import flup.server.fcgi

def myapp(environ, start_response):
    status = '200 OK'
    response_headers = [('Content-type','text/plain')]
    start_response(status, response_headers)
    return ['Hello World!\n']

if __name__ == '__main__':
    flup.server.fcgi.WSGIServer(myapp).run()


7. Простое web.py приложение с использованием flup:
/var/www/app.py:
#! /usr/bin/python
import web
urls = (  '/', 'index', )

class index:
    def GET(self):
        return "Hello World!"

if __name__ == '__main__':
    web.application(urls, globals()).run()

приложение станет доступным по адресу localhost/app.py

8. По умолчанию web.py использует flup, но можно обойтись и без него.
Для запуска web.py на wsgiref необходимо:
web.application(urls, globals()).cgirun()

B ссылках на скрипты web.py в конце не забывать ставить '/' (app.py/), иначе ответом будет «not found». По-хорошему необходимо создать rewrite правило:
# mod_rewrite configuration.
url.rewrite-once = (
    "^/favicon.ico$" => "/favicon.ico", 
    "^/(.*)$" => "app.py/$1" ,)

Для отладки в скриптов полезно добавить:
import cgitb
cgitb.enable()

тогда будут видны ошибки.

Остается опробовать:
modwsgi
paste
pylons

Полезные ссылки:
WSGI wiki
wep.py
WSGI — протокол связи Web-сервера с Python приложением
WSGI, введение
How to serve a WSGI application via CGI
WSGI.org
Сравнение эффективности способов запуска веб-приложений на языке Python