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

    Часть 1.
    Часть 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

    Similar posts

    Ads
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More

    Comments 0

    Only users with full accounts can post comments. Log in, please.