Часть 1.
Часть 2.
В этой статье речь пойдет о тонкостях реализации wsgi приложения. Мы не будем рассматривать написание сервера приложений с нуля, поскольку есть масса микрофреймворков которые это уже делают, не говоря о крупных. Для этих целей был выбран bottle. В основном за его минималистичность. Также мы поговорим о роутах, статике, и сессиях которыми заведовать будет beaker.
Одна из основных проблем микрофреймворков в том что при возникновении ошибок в приложении, большинство ошибок которые нам выдает wsgi надо отлавливать в логах. А нам бы хотелось все это видеть на странице в браузере, или в его консоли в случае если какой нибудь post запрос вернул ошибку.
Для этого мы просто встроимся в цепочку обработки и будем перехватывать все исключения, которые не перехватило само приложение, и выдавать его как красиво оформленную страницу.
Класс который непосредственно обслуживает наш middleware.
Сама функция которая рисует нашу ошибку:
По умолчанию мы уже имеем в bootle роуты как декоратор который можно цеплять на любую функцию:
но это удобно для относительно небольших проектов. А нам бы хотелось расширить этот момент чтобы можно было в любом компоненте создать файлик такого типа.
Соответственно, после этого, можно в любом месте, желательно в этом компоненте, уже вызвать функцию hello. Для этого напишем в файле в котором содержится описание основного функционала ядра.
Теперь заменяем комментарий который мы оставляли в первой части посвященной отладке на вызов инициализацию роутов, чтоб они у нас сразу же были подключены.
Все теперь у нас есть полноценные роуты мы можем при желании любую функцию обернуть в декоратор, или же изложить в файлике route.py список роутов с назначенными им функциями и при желании передать им нужные параметры через лямбду.
Также теперь с помощью ключа нашего словаря routes в данному случае 'function2' мы можем формировать ссылки по названию роута не зная точно какие параметры будут переданы в саму ссылку например если взять '/hello/<_id>' то <_id> будет задан динамически.
Теперь пишем в любом месте в шаблоне или в просто в коде
Признатся часто бывает проще написать просто ссылку но иногда без этой функции не обойтись.
Работа с сессиями заключается в использовании beaker. Этот замечательный модуль умеет работать с сессиями, сохранять их в выбраную папку или в базу. Можно настроить чтоб он сохранял их например в postgresql или в memcached. Первое что мы сделаем это импорт:
Далее заменяем комментарий на инициализацию сразу после инициализации роутов.
И в функцию pre_init(), добавляем строчку для динамического если так можно сказать определения для какого домена нам хранить сессии.
get_name_host() — занимается получением названия нашего сайта.
После этого все что нам нужно это простая функция с помощью которой можно будет пользоваться сессиями.
Теперь в любом месте:
Статикой у нас также будет заниматься bottle, только мы в свою очередь расширим его возможности для своих нужд.
Теперь осталось научится подключать статику при желании из скриптов модулей если модуль такой существует, а не все без разбору в основном шаблоне.
Вызываем в любом модуле эту функцию и передаем в качестве первого аргумента правильный путь статическому файлу такого типа '/static/<component>/<fname:re:.*>':
А в основном шаблоне просто вызываем:
Сейчас простой Hello world будет выглядеть примерно так.
Файлик с роутами route.py разместим в /app/app_one проекта:
Там же рядом размещаем файлик view.py хотя название тут уже не принципиально разве что с точки зрения логики:
И в этом же каталоге в папку /templ кладем шаблон с названием hello.tpl.
Все заходим в корень сайта и видим приветствие.
Собственно основной каркас готов. Некоторые фреймворки во общем в той или иной степени реализуют то что мы рассматривали в этой серии уроков. Но мне кажется что было интересно посмотреть на один из вариантов как это может быть реализовано. Следующая часть будет посвящена созданию админки а также представлению данных.
Пока все. Всем успехов.
Bottle
Beaker и кеширование
Beaker
Введение в веб для python
Python Web Server Gateway Interface
Часть 2.
Введение.
В этой статье речь пойдет о тонкостях реализации wsgi приложения. Мы не будем рассматривать написание сервера приложений с нуля, поскольку есть масса микрофреймворков которые это уже делают, не говоря о крупных. Для этих целей был выбран bottle. В основном за его минималистичность. Также мы поговорим о роутах, статике, и сессиях которыми заведовать будет beaker.
Отладка
Одна из основных проблем микрофреймворков в том что при возникновении ошибок в приложении, большинство ошибок которые нам выдает 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>' % ' '.join(['%s=%s' %(k, ee[k])for k in ee]) 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)
Признатся часто бывает проще написать просто ссылку но иногда без этой функции не обойтись.
Работа с сессиями
Работа с сессиями заключается в использовании 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/<component>/<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 app.app_one.view import * routes = { 'hello': ('/', hello, 'GET') }
Там же рядом размещаем файлик view.py хотя название тут уже не принципиально разве что с точки зрения логики:
def hello(): text = 'It works' return templ('hello', text=text)
И в этом же каталоге в папку /templ кладем шаблон с названием hello.tpl.
{{text}}
Все заходим в корень сайта и видим приветствие.
Резюме
Собственно основной каркас готов. Некоторые фреймворки во общем в той или иной степени реализуют то что мы рассматривали в этой серии уроков. Но мне кажется что было интересно посмотреть на один из вариантов как это может быть реализовано. Следующая часть будет посвящена созданию админки а также представлению данных.
Пока все. Всем успехов.
Используемые и полезные материалы
Bottle
Beaker и кеширование
Beaker
Введение в веб для python
Python Web Server Gateway Interface