Введение.
В этой статье речь пойдет о тонкостях реализации wsgi приложения. Мы не будем рассматривать написание сервера приложений с нуля, поскольку есть масса микрофреймворков которые это уже делают, не говоря о крупных. Для этих целей был выбран bottle. В основном за его минималистичность. Также мы поговорим о роутах, статике, и сессиях которыми заведовать будет beaker.
1. Отладка
Одна из основных проблем микрофреймворков в том что при возникновении ошибок в приложении, большинство ошибок которые нам выдает wsgi надо отлавливать в логах. А нам бы хотелось все это видеть на странице в браузере, или в его консоли в случае если какой нибудь post запрос вернул ошибку.
Для этого мы просто встроимся в цепочку обработки и будем перехватывать все исключения, которые не перехватило само приложение, и выдавать его как красиво оформленную страницу.
try:
# говорим bottle что перед тем как вызвать любой обработчик роута, нужно выполнить указаную функцию.
@bottle.hook('before_request')
def pre_init():
# в данном случае у нас создается контекст,
ini_environment()
# Тут будет вызов функции инициализирующей роуты
# Инициализация options_session
# создаем приложение bottle
app = bottle.app()
app.catchall = False
# вызываем созданый нами класс для перехвата исключений.
debugger = DebuggerM(app)
# Встраиваем в цепочку обработчик сеансов.
session = SessionMiddleware(debugger, options_session)
# через переменную application передаем нашу цепочку wsgi'ю
application = session
except Exception as e:
# если не удалось инициализировать цепочку то мы назначаем специальный упрощеный обработчик
# который должен показать что произошло.
exc = sys.exc_info()
def error_apps(environ, start_response):
start_response('500 INTERNAL SERVER ERROR', [('Content-Type', 'text/html; charset=utf-8')])
return view_error(exc, environ, e)
application = error_apps
Класс который непосредственно обслуживает наш middleware.
class DebuggerM(object):
""" Встраивается в цепочку вызовов и ловит исключения"""
__app = None
def __init__ (self, app): # Получаем приложение по цепочке из bottle
# app-параметр конструктора передается при создании, это application через который идут вызовы middleware
self.__app = app
def __call__(self, environ, start_response):
try:
# передаем управление дальше по цепочке (ничего дополнительного не встраиваем)
app_iter = self.__app(environ, start_response)
for item in app_iter:
# передаем управление изначально вызвавшей программе
# возвращает значение назад по цепочке и ждет следующего вызова
yield item
except Exception as e:
# если поймали исключение то выдаем ошибку в качестве результата.
start_response('500 INTERNAL SERVER ERROR', [('Content-Type', 'text/html; charset=utf-8')])
yield view_error(sys.exc_info(), environ, e)
Сама функция которая рисует нашу ошибку:
def view_error(exc, environ, e):
import cgi, traceback
ee = dict(environ)
text= ''
text='<h1>ERROR "%s"</h1>' % str(e)
text+='<style>pre{border:red solid 1px; max-height:240px; overflow:auto;}</style>'
text+='<h2>Stack trace</h2><pre>%s</pre>' % ''.join(reversed(traceback.format_exception(*exc)))
text+='<h2>Env</h2><pre">%s </pre>' % '<br/>'.join(['%s = %s' % (k, ee[k]) for k in ee])
text+='<h2>Python path</h2><pre>%s </pre>' % '<br />'.join(sys.path)
text+='</pre>'
return text
Работа с роутами
По умолчанию мы уже имеем в bootle роуты как декоратор который можно цеплять на любую функцию:
@route('/hello/:name')
def index(name='World'):
return '<b>Hello %s</b>' % name
но это удобно для относительно небольших проектов. А нам бы хотелось расширить этот момент чтобы можно было в любом компоненте создать файлик такого типа.
from app1 import *
routes = {
'function': ('/hello/', hello, 'GET'),
'function2': ('/hello/<_id>', hello_post, 'POST')
}
Соответственно, после этого, можно в любом месте, желательно в этом компоненте, уже вызвать функцию hello. Для этого напишем в файле в котором содержится описание основного функционала ядра.
all_routes = {}
def union_routes(dir):
routes = {}
# заносим в глобальный импорт словарь в котором будут содержатся уже все роуты собранные по компонентам.
if __builtin__.__import__('routes', globals=globals()):
# получаем все модули
module = sys.modules['routes']
# добавляем их в словарь routes
routes.update(module.routes)
# проходимся по всем директориям
for name in os.listdir(dir):
path = os.path.join(dir, name)
# если у нас там есть файл routes.py
if os.path.isdir(path) and os.path.isfile(path+'/routes.py'):
# сооздаем выражение для импорта и заносим его в глобальный импорт
name = 'app.'+path[len(dir)+1:]+'.routes'
if __builtin__.__import__(name, globals=globals()):
module = sys.modules[name]
routes.update(module.routes)
return routes
def setup_routes(routes):
# тут мы непосредствено берем все собраные роуты и скармливаем их bottle собственно немного расширяем его возмоности для своих нужд.
all_routes.clear()
all_routes.update(routes)
for name in routes:
path, func, method = routes[name]
route(path, method=method, name=name)(func)
Теперь заменяем комментарий который мы оставляли в первой части посвященной отладке на вызов инициализацию роутов, чтоб они у нас сразу же были подключены.
routes = {}
# инициализируем роуты расположенные в библиотеке
routes.update(union_routes(lib_path))
# инициализируем роуты расположенные в приложении
routes.update(union_routes(app_path))
setup_routes(routes)
Все теперь у нас есть полноценные роуты мы можем при желании любую функцию обернуть в декоратор, или же изложить в файлике route.py список роутов с назначенными им функциями и при желании передать им нужные параметры через лямбду.
from app1 import *
routes = {
'function2': ('/hello/<_id>', hello_post, 'POST'),
'function3': ('/base/1', lambda: test(True, u'тест'), 'GET')
}
Также теперь с помощью ключа нашего словаря routes в данному случае 'function2' мы можем формировать ссылки по названию роута не зная точно какие параметры будут переданы в саму ссылку например если взять '/hello/<_id>' то <_id> будет задан динамически.
def make_link(name, params, full=False):
""" Вырезаем все что находится <> и возвращаем готовую ссылку """
# получаем шаблон самой ссылки
templ_link = all_routes[name][0][1:]
# находим все что между символами <>
r = re.compile(r'<([^:>]+)(:[^>]+)?>')
# пока мы в шаблоне находим подобные блоки, мы их заменяем на значение соответствующих переменных.
while True:
rs = r.search(templ_link)
if not rs: break
sub = rs.group(0)
name = rs.group(1)
templ_link = re.sub(sub, params[name], teml_link)
link = os.path.sep+ templ_link
# если передан параметр тру то формируем полную сылка, если нет то остается относительная.
if full:
link = 'http://'+get_name_host()+link
return link
Теперь пишем в любом месте в шаблоне или в просто в коде
link = make_link('test', {'id':id, 'doc_id':doc_id}, True)
Признатся часто бывает проще написать просто ссылку но иногда без этой функции не обойтись.
1. Работа с сессиями
Работа с сессиями заключается в использовании beaker. Этот замечательный модуль умеет работать с сессиями, сохранять их в выбраную папку или в базу. Можно настроить чтоб он сохранял их например в postgresql или в memcached. Первое что мы сделаем это импорт:
from beaker.middleware import SessionMiddleware
Далее заменяем комментарий на инициализацию сразу после инициализации роутов.
options_session = {
'session.cookie_expires': 30000,
'session.timeout': 30000,
'session.type': 'file', # вариант куда сохраняются сесии
'session.data_dir': './s_data' # папка в которой сохранятся сессии
}
И в функцию pre_init(), добавляем строчку для динамического если так можно сказать определения для какого домена нам хранить сессии.
session.options['session.cookie_domain'] = get_name_host()
get_name_host() — занимается получением названия нашего сайта.
После этого все что нам нужно это простая функция с помощью которой можно будет пользоваться сессиями.
def session():
# получаем контейнер сессии
s = request.environ.get('beaker.session')
return s
Теперь в любом месте:
s = session()
s['test'] = '123'
s.save()
Статика
Статикой у нас также будет заниматься bottle, только мы в свою очередь расширим его возможности для своих нужд.
# Говорим bottle с помощью его декораторов какие ссылки должны отвечать за статику, и если он встречает такую ссылку то он вызывает эту функцию.
@route('/static/<component>/<fname:re:.*>')
def st_file(component, fname):
# проверяем файл по соответствующему пути и заносим в переменную которую передаем потом соответствующей функции, отвечающей за статику у bottle
path = os.path.join( settings.lib_path, component, 'static') + os.path.sep
if not os.path.exists(path + fname):
path = os.path.join( settings.lib_path, 'app', component,'static')+ os.path.sep
if not os.path.exists( path + fname):
path = os.path.join( os.getcwd(), 'app', component, 'static')+ os.path.sep
if not os.path.exists(path + fname) and component == 'static':
path = os.path.join( os.getcwd(), 'static')+ os.path.sep
return static_file(fname, root=path)
Теперь осталось научится подключать статику при желании из скриптов модулей если модуль такой существует, а не все без разбору в основном шаблоне.
def add_resourse(name, append=True):
env = get_environment(); t = ''
if name.endswith('.css'): t = 'css'
if name.endswith('.js'): t = 'js'
if not t: return
if not name in env['res_list'][t]:
if append: env['res_list'][t].append(name)
else: env['res_list'][t].prepend(name)
Вызываем в любом модуле эту функцию и передаем в качестве первого аргумента правильный путь статическому файлу такого типа '/static//<fname:re:.*>':
add_resourse(/static/base/base.js, True)
А в основном шаблоне просто вызываем:
{% for res in env.res_list['js'] %}
<script type="text/javascript" src="{{res}}"></script>
{% endfor %}
{% for res in env.res_list['css'] %}
<link rel="stylesheet" type="text/css" href="{{res}}" />
{% endfor %}
Hello world
Сейчас простой Hello world будет выглядеть примерно так.
Файлик с роутами route.py разместим в /app/app_one проекта:
from perm import *
routes = {
'hello': ('/', hello, 'GET')
}
Там же рядом размещаем файлик view.py хотя название тут уже не принципиально разве что с точки зрения логики:
def hello():
text = 'Its worcks'
return templ('hello', text=text)
И в этом же каталоге в папку /templ кладем шаблон с названием hello.tpl.
{{text}}
Все заходим в корень сайта и видим приветствие.
Резюме .
Собственно основной каркас готов. Некоторые фреймворки во общем в той или иной степени реализуют то что мы рассматривали в этой серии уроков. Но мне кажется что было интересно посмотреть на один из вариантов как это может быть реализовано. Следующая часть будет посвящена созданию админки а также представлению данных.
Пока все. Всем успехов.
Используемые и полезные материалы
Bottle
Beaker и кеширование
Beaker
Введение в веб для python
Python Web Server Gateway Interface
Автор: Alex10