dummy_wsgi_framework

Dummy WSGI Framework is base for your Web-applications or your own WSGI-framework


Install
pip install dummy_wsgi_framework==0.0.13

Documentation

Dummy WSGI Framework

Dummy WSGI Framework - это WSGI-фрейморк для ваших Веб-приложений или основа для вашего собственного WSGI-фреймворка, он "простенький" и "глупенький" (хотя на самом деле он не так уж "прост" и "глуп", как может показаться на первый взгляд), разработан как Just4Fun-проект.

Dummy WSGI Framework методологически состоит из "контроллеров" и "представлений", как паттерн "Model-View-Controller", но без "Model".

Он позволяет работать с представлениями, генерируемыми внутри контроллеров или подгружаемыми из файлов, например, в базовом варианте фреймворка это просто HTML-файлы.

Dummy WSGI Framework расширяем:

  • в силу открытого доступа к WSGI-интерфейсу на уровне контроллеров приложения, разработанного на его (фреймворка) основе;
  • в силу возможности использования произвольных шаблонизаторов за счет реализованного в ядре фреймворка декоратора функции загрузки файлов представлений;
  • и, в конце концов, это же Python, тут многое возможно.

Вы можете свободно испольтзовать Dummy WSGI Framework. Ссылаться на факт использования Вами Dummy WSGI Framework не обязательно, но мне бы хотелось иметь публичный отзыв, даже если он и не положительный.

Оглавление текущего описания

Требования

Использует версию Python 3.4

Установка

Dummy WSGI Framework включен в реест пакетов PyPI (Python Package Index) - https://pypi.org/project/dummy_wsgi_framework/

Его установка возможна с использованием pip

$ pip install dummy_wsgi_framework

Или иным возможным образом, например (команда для OS Debian):

$ cd <your_project_dir>
$ git clone git://github.com/BorisPlus/dummy_wsgi_framework.git

или, например, по ссылке для скачивания master-ветки проекта Zip (команда для OS Debian):

$ wget https://github.com/BorisPlus/dummy_wsgi_framework/archive/master.zip

↑ наверх в оглавление

Руководство разработчика

Директория dummy_wsgi_framework - это и есть фреймворк с функциональным ядром в директории dummy_wsgi_framework/core и примером базового одностраничного приложения в директории dummy_wsgi_framework/app.template.

Для быстрого старта:

  1. Создайте директорию вашего приложения app (где угодно).
  2. Cкопируйте в директорию app содержимое директории dummy_wsgi_framework/app.template.
  3. Запустите приложение:
$ uwsgi --http 127.0.0.1:8080 --wsgi-file /<absolute_path>/app/application.py

↑ наверх в оглавление

Описание структуры приложения

Конфигурация

Файл config.py вашего приложения содержит конфигурационные переменные, и предпочтительно, без должного опыта, не изменять существующие их значения (хотя это возможно, см. конфигурацию примера "Приложение с переопределенными путями хранения файлов контроллеров и представлений") и добавлять свои конфигурационные переменные именно сюда.

↑ наверх в оглавление

Маршруты

Файл routes.py приложения содержит так называемые "маршруты" - соответствие HTTP-запросов (на самом деле их масок) и контроллелов приложения.

Маршруты задаются в виде:

routes_of_uri_regexps = (
    dict(uri_regexp='/index/', controller='index.py'),
    dict(uri_regexp='/', controller='index.py'),
)

или с подгрузкой базовых маршрутов фреймворка:

from dummy_wsgi_framework.core.routes import base_routes_of_uri_regexps

routes_of_uri_regexps = tuple([
    dict(uri_regexp='^/page_one/$',
         controller='page_one.py'),
    dict(uri_regexp='^/page_two/$',
         controller='page_two.py')
    ] + list(base_routes_of_uri_regexps).copy())

, где:

  • значение ключа uri_regexp - это REQUEST_URI, последующая после доменного имени или IP-адреса часть HTTP-запроса. Знак косой черты "/" в конце uri_regexp обязателен, так как диспетчер контроллеров через функционал ядра фреймворка контролирует маршруты HTTP-запросов, и если в конце REQUEST_URI знак косой черты "/" отсутствует, то происходит редирект (перенаправление HTTP-запроса) на REQUEST_URI с приписанной в конец косой чертой "/".
  • значение ключа controller - это сам Python-файл контроллера, соответствующего uri_regexp и реализующего логику реакции на HTTP-запрос.

Будте внимательны, срабатывает первый по порядку (сверху вниз) маршрут. Алгоритм сопоставления опирается на успех функции re.compile(uri_regexp).findall(REQUEST_URI). Функционал регулярных выражений в uri_regexp был введен в ядро фреймворка для возможности назначения и использования в маршрутах именованных параметров HTTP-запросов ("Приложение с оработкой допущенных параметров").

↑ наверх в оглавление

Диспетчер контроллеров

За направление HTTP-запросов к контроллерам в соответствии с маршрутами из routes.py отвечает функция application() - так называемый "диспетчер контроллеров" приложения, расположенный в application.py (файл называть не обязательно именно так). Этот файл выступает в качестве входной точки вашего WSGI-приложения.

# Content of: app/application.py
import config as app_config
from dummy_wsgi_framework.core.dispatchers import get_controller_response

def application(environ, start_response):
    return get_controller_response(environ, start_response, app_config)

В итоге приложение запускается так:

$ uwsgi --http 127.0.0.1:8080 --wsgi-file /<absolute_path>/app/application.py

Таким образом у Вас есть возможность создать на основе Dummy WSGI Framework несколько отдельных приложений, запуская их на разных портах сервера.

↑ наверх в оглавление

Контроллеры

"Контроллеры" это Python-файлы, реализующие логику реакции на HTTP-запросы в соответствии с объявленными маршрутами.

Создайте свои контроллеры, реализовав их логику работы (можно взять типовые с подгрузкой шаблонов и без, или реализовать свои с посылкой специальных HTTP-заголовков, например, для Basic Access Authentication как в примере "Приложения с двумя секретными страницами (Basic access authentication)").

Контроллеры размещаются в директории controllers в корневой директории приложения (что не обязательно, так как в config.py можно переопределить базовое расположение файлов контроллеров, см. конфигурацию примера "Приложение с переопределенными путями хранения файлов контроллеров и представлений").

Основным методом в программном протоколе вызова контроллера приложения является функция get_response(). Обязательно реализуйте ее, иначе диспетчер контроллеров не сможет вызвать соответствующий контроллер, и будет сгененрировано исключение ControllerFileIsInvalid с указанием отсутствия у контроллера функции get_response()

Контроллеры "редиректа" и "404-ой ошибки" реализованы в ядре фреймворка:

# File: core/controllers/redirect.py
def get_response(environ, start_response, app_config, location):
    start_response('301 Moved Permanently', [('Location', location)])
    return b''

и

# File: core/controllers/error404.py
def get_response(environ, start_response, app_config, message):
    start_response('404 Not found', [('Content-Type', 'text/html; charset=utf-8')])
    return [
        # some template
    ]

↑ наверх в оглавление

Представления

Контроллеры могут непосредственно овечать на HTTP-запрос или же загружать заранее подготовленные в приложении файлы представлений, иногда их так же называют "шаблоны".

Python-файлы представлений размещаются в директории views в корневой директории приложения (что не обязательно, так как в config.py можно переопределить базовое расположение файлов представлений, см. конфигурацию примера "Приложение с переопределенными путями хранения файлов контроллеров и представлений").

Подробное описание реализации загрузки контроллером файла представления приведено здесь в примере "Демонстрация базовых_возможностей фреймворка".

↑ наверх в оглавление

Описание логики работы приложения

Точка входа вашего приложения (app/application.py: def application()...), передаст полученные аргументы базовому диспетчеру контроллеров (core/dispatchers.py: def get_controller_response()...) вместе с отсылкой на конфигурационные данные текущего приложения. Базовый диспетчер контроллеров в соответствии с переданным ему REQUEST_URI по имеющимя данным в маршрутах текущего приложения (app/routes.py: routes_of_uri_regexps) вызовет по успеху (core/routes.py: def get_controller_by_uri_regexp()...) соответствующий данному маршруту контроллер (app/controllers/ACTION.py: def get_response()), который в свою очередь ответит представлением по "зашитому" в него функционалу или передаст (если вы так реализуете) полученные им параметры дальше методу (core/dispatchers.py: def load_view()..._), который "загрузит" файл вашего представления (app/views/ACTION.html).

↑ наверх в оглавление

Примеры

Полный список примеров

Примеры разработанных на основе Dummy WSGI Framework приложений:

↑ наверх в оглавление

Пример "Демонстрация базовых возможностей фреймворка"

Пример "Демонстрация базовых_возможностей фреймворка" содержит демонстрационный вариант Веб-приложения, а именно:

  • реализацию вызова контроллера без представления;
  • реализацию вызова контроллера с загрузкой представления из HTML-файла;
  • демонстрацию штатного отлова на уровне ядра фреймворка исключения RouteDoesNotExist, возникающего при заведомо отсутствующем маршруте, удовлетворяющего пришедшему HTTP-запросу;
  • демонстрацию штатного отлова на уровне ядра фреймворка исключения ControllerFileDoesNotExist, возникающего при заведомо отсутствующем файле контроллера, маршрут до которого существует;
  • демонстрацию штатного отлова на уровне ядра фреймворка исключения ControllerFileIsInvalid, возникающего при заведомо отсутствующей у файла контроллера, маршрут до которого существует, функции get_response();
  • демонстрацию штатного отлова на уровне ядра фреймворка исключения ViewDoesNotExist, возникающего при заведомо отсутствующем файле представления, подгружаемом в функции get_response() контроллера, маршрут до которого существует.

Кроме того, с целью возможного использования разработчиками каких-либо шаблонизаторов отдельно продемонстрирвана возможность декорирования функции загрузки файла представления:

  • реализация "как есть" - загрузка файла представления, реализованная в ядре фреймворка;
  • реализация с использованием "чего-то" самописного - загрузка файла представления и замена сответствующих лексем, если таковые переданы;
  • реализация с использованием Jinja2 - загрузка файла представления и его обработка с помощью Jinja2.

Мне не хотелось вносить зависимость от Jinja2 в ядро фреймворка, так как я считаю подобную связь слишком жесткой, да и Dummy WSGI Framework изначально задумывался как "базовый", "каркасный", "простенький" и "глупенький". При этом, понимая, что этого может быть не достаточно сторонним разработчикам, и имея интерес по реализации подобной архитектуры фреймворка, я реализовал в ядре фреймворка декоратор для функции загрузки файла представления.

Декоратор @decorate_loaded_view_function_for_response производит проверку существования файла представления и передает функции обработки этого файла полученные ею аргументы.

То есть для загрузки файла представления "как есть", реализованной в ядре фреймворка, предполагается вызов в контроллере функции load_view() ядра фреймворка:

# Framework solution
from dummy_wsgi_framework.core.dispatchers import load_view

def get_response(environ, start_response, app_config):
    return load_view(
        environ,
        start_response,
        app_config,     # указываем базовому диспетчеру контроллеров
                        # какое из ваших приложений использовать
        'load_me.html'  # имя файла представления, 
                        # обычно в качестве данного параметра я использую вызов
                        # 
                        # from dummy_wsgi_framework.core.dispatchers import (
                        #   resolve_name_by_python_file_name
                        # )
                        # resolve_name_by_python_file_name(__file__, '%s.html')
                        # 
                        # что обяжет файл представления иметь тоже имя, 
                        # что и у файла контроллера и упростит возможный рефакторинг
    )

А для "чего-то" самописного у вас есть возможность реализовать функцию загрузки файла представления, например, с заменой сответствующих лексем шаблона. Необходимо лишь применить к вашей функции загрузки файла представления декоратор @decorate_loaded_view_function_for_response:

# Your self realization
from dummy_wsgi_framework.core.dispatchers import decorate_loaded_view_function_for_response

@decorate_loaded_view_function_for_response
def yourself_load_view_function(view_path, **kwargs):
    with open(view_path, 'rb') as f:
        response_body = f.read()
        for k in kwargs:
            response_body = response_body.replace(
                ("{{ %s }}" % k).encode(), 
                str(kwargs[k]).encode()
            )
    return response_body

def get_response(environ, start_response, app_config):
    return yourself_load_view_function(
        environ,
        start_response,
        app_config,
        'load_me.html',     # файл представления, шаблон
        id=1,               # некие переменные, которые будут переданы в шаблон и
        value=2,            # будут участвовать в замене в response_body.replace
        other=4
    )

Для использования шаблонизаторов, например, Jinja2, вышеописанный код может выглядеть так:

# Jinja2 realization
from dummy_wsgi_framework.core.dispatchers import (
    decorate_loaded_view_function_for_response
)
from dummy_wsgi_framework.core.exceptions import (
    ExistViewFileIsInvalid, 
    ViewDoesNotExist
)
import config
import jinja2
import os

@decorate_loaded_view_function_for_response
def load_jinja2_view_template(view_template_path, **kwargs):
    try:
        environment = jinja2.Environment(loader=jinja2.FileSystemLoader(config.APP_VIEWS_DIR))
        template = environment.get_template(os.path.basename(view_template_path))
        return template.render(**kwargs).encode()
    except jinja2.exceptions.TemplateSyntaxError as e:
        raise ExistViewFileIsInvalid('File "%s" - %s' % (view_template_path, e.message))
    except jinja2.exceptions.TemplateNotFound as e:
        raise ViewDoesNotExist('File "%s" not found %s' % (view_template_path, e.message))

def get_response(environ, start_response, app_config):
    return load_jinja2_view_template(
        environ,
        start_response,
        app_config,
        'load_me.html',     # шаблон в формате Jinja2, возможно с базовым.
        id=1,               # некие переменные, которые будут переданы в шаблон и
        value=2,            # будут участвовать в замене в template.render
        other=4
    )

Рекомендую выносить функции, подобные load_jinja2_view_template(), в разрабатываемом приложении в отдельный файл, например, как здесь - user_def.py, так как это представляется методологически верным для повторного использования функций, подобных load_jinja2_view_template(), например, для использования load_jinja2_view_template() в других контроллерах.

↑ наверх в оглавление

Пример 'Приложение с двумя секретными страницами (Basic access authentication)'

Когда выше говорилось, что Dummy WSGI Framework расширяем в силу открытого доступа к WSGI-интерфейсу на уровне контроллеров приложения, то имелось в виду, что у разработчика существует возможность организовать посыл HTTP-заголовков и иной служебной информации непосредственно в контроллерах своего приложения, разработанного на основе Dummy WSGI Framework. Тому может служить пример "Приложение с двумя секретными страницами (Basic access authentication)"

Замечание о протоколе запроса логина и пароля:
* Веб-приложение (в нашем случае это отдельный контроллер) пошлет клиенту 
  HTTP-заголовок запроса Basic авторизации.
* Браузет продемонстрирует форму.
* Вы введете логин и пароль.
* Они конкатенируются с использованием двоеточия ":", закодируются 
  браузером по протоколу Base64 и передадутся на сервер.
* Приложение должно будет декодировать и проверить на валидность переданную 
  пару "login:password".
* В случае успеха Вы получите доступ к "секретной" информации :)

Пусть маршруты будут таковы:

routes_of_uri_regexps = (
    dict(uri_regexp=r'^/index_1/$', controller='first_secret_page.py'),
    dict(uri_regexp=r'^/index_2/$', controller='second_secret_page.py'),
    dict(uri_regexp=r'^/$', controller='first_secret_page.py'),
)

Контроллер:

# Content of 'first_secret_page.py'
import base64

def get_response(environ, start_response, app_config):
    if environ.get('HTTP_AUTHORIZATION', '').startswith('Basic '):  # пришел ли заголовок
        login_password_at_b64 = environ.get('HTTP_AUTHORIZATION', '')[6:]
        login_password = base64.b64decode(login_password_at_b64)  # декодируем
        if login_password == b'user:user':
            start_response('200 OK', [('Content-Type', 'text/html; charset=utf-8')])
            return [
                # some template
            ]
    # "login:password" is empty or not valid
    start_response('401 Access Denied', [
        ('Content-Type', 'text/html; charset=utf-8'),
        ('WWW-Authenticate', 'Basic realm="Dummy WSGI Framework"'),
        ('Content-Length', '0'),
    ])
    return b'Basic Auth'

А на "второй секретной" странице может быть совершенно другие "login:password", например, "admin:admin".

↑ наверх в оглавление

Пример 'Приложение с оработкой допущенных параметров'

С целью возможной необходимости использования вами в вашем приложении параметров HTTP-запросов был реализован механизм назначения именнованных параметров (в нотации "name=value") для маршрута, допустимых в HTTP-запросе и необходимых для его контроллера, с последующей их "передачей" дальше в представление (в той же нотации "name=value"). Это продемонстировано в примере "Приложения с оработкой допущенных параметров"

Ключевым моментом для использования параметров является правильное написание регулярного выражения в соответствующем uri_regexp.

routes_of_uri_regexps = (
    ...
    dict(uri_regexp=r'^(/view_random/\?(id=[0-9]*)&(value=[A-F0-9]*)/)$', controller='view.py'),
    ...
)

При формировании в uri_regexp регулярного выражения необходимо придерживаться следующих правил:

  • сформировать корневой HTTP-путь, так называемый PATH_INFO, например, "/path/info/"
  • определиться с набором параметров, передаваемых контроллеру, указанному в ключе controller, например, это "id", "hex", "word"
  • сформировать пары из параметров и их шаблонных типов значений, например, "id=id_value_type", "hex=hex_value_type", "word=word_value_type"
  • заключить пары в скобки и сконкатенировать их, используя "&", например, "(id=id_value_type)&(hex=hex_value_type)&(word=word_value_type)"
  • сконкатенировать полученное с корневым HTTP-путем, используя "\?", например, "/path/info/\?(id=id_value_type)&(hex=hex_value_type)&(word=word_value_type)"
  • поставить в конце знак косой черты "/", например, "/path/info/\?(id=id_value_type)&(hex=hex_value_type)&(word=word_value_type)/"
  • заключить в круглые скобки, например, "(/path/info/\?(id=id_value_type)&(hex=hex_value_type)&(word=word_value_type)/)"
  • поставить знаки начала "^" и конца "$" регулярного выражения, например, "^(/path/info/\?(id=id_value_type)&(hex=hex_value_type)&(word=word_value_type)/)$"
  • заменить шаблонные типы значений на их шаблоны в нотации регулярных выражений, например:
    • для "id_value_type" - "[0-9]*"
    • для "hex_value_type" - "[0-9A-Fa-f]*"
    • для "word_value_type" - "[0-9A-Za-z_]*" или "\w*"
  • как итог в uri_regexp внести "^(/path/info/\?(id=[0-9]*)&(hex=[0-9A-Fa-f]*)&(word=[0-9A-Za-z_]*)/)$"

В текущем примере контроллер list генерирует представление со списком ссылок, где указаны шестнадцатиричные значения в верхнем регистре и их id, присвоенные им в порядке их генерации.

А контроллер view отображает переданные в HTTP-запросе id и шестнадцатиричное значение в верхнем регистре . Можете проверить (или поверить), наличие необходимых параметров с недопустимыми значениями, даже шестнадчатиричными в нижнем регистре, а также отсутствие необходимых параметров или наличие иных параметров приведут в вызову исключения RouteDoesNotExist, ведь действительно нет маршрута удовлетворяющего подобному uri_regexp.

↑ наверх в оглавление

Лицензия

Вы можете свободно испольтзовать Dummy WSGI Framework в качестве каркаса для своих Веб-приложений или основы для своего Веб-фреймворка. Ссылаться на факт использования Вами Dummy WSGI Framework не обязательно, но мне бы хотелось иметь публичный отзыв, даже если он и не положительный.

↑ наверх в оглавление

Дополнительные сведения

Проект был начат в рамках домашнего задания курса "Web-разработчик на Python" на https://otus.ru/learning и продолжен как Just4Fun-проект.

Изначально было решено не использовать классы, так как о них при постановке задачи не говорилось, а также не использовать и сторонние библиотеки, чтобы Dummy WSGI Framework был максимально "чистым".

Прошу понять и простить.

Теперь, надеюсь, и Вам кажется, что Dummy WSGI Framework не так уж "прост" и "глуп".

↑ наверх в оглавление