Django своими руками часть 3: Роуты, отладка, middleware, bottle, beaker

в 16:16, , рубрики: framework, python, метки: ,

Часть 1.
Часть 2.

Введение.

В этой статье речь пойдет о тонкостях реализации 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

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js