Svarga - еще один web фреймворк

Примерно год назад решил сделать небольшой сайтик для себя. Думал на чем же делать (первоначально хотел на ASP.NET, но оный уже на работе набил оскомину), так что решил писать на питоне с использованием Django. Предупреждаю сразу, пост сугубо технический и предполагает значение Django в какой-либо степени.

Django, конечно, классная штука, но через пару недель разработки столкнулся с тем, что я не могу сделать множество вещей или “просто” или “эффективно”.

Пример таких вещей:

  • Странная система настроек доступа в contrib.auth - права доступа прописываются на уровне моделей
  • Убогие возможности по разграничению доступа к различным частям админки
  • Админка ориентирована на управление данными (читай - моделями) и не представляет собой некий фреймворк для построения админок
  • Тормоза в contrib.auth при использовании профилей (надо патчить модель пользователя)
  • Убогие возможности ORM’ки (те же агрегации появились уже после того, как я решил делать сайт) и т.д.
  • Тормоза в шаблонах, особенно при наследовании
  • Ужасные forms
  • Смесь логики представления с моделями данных (см. Meta в моделях)
  • И так далее.

В общем, посидели мы с Пираньей и решили что надо что-то делать. Текущие фреймворки а-ля Pylons как-то не нравились тем, что они представляют собой сборную солянку, которую все равно надо собирать. А такого, чтобы взял и запустил проект с минимальными телодвижениями - не нашли.

В результате, было принято решение делать фреймворк который внешне похож на Django, но без определенных его недостатков. За основу взяли:

  • Werkzeug как основу WSGI приложения
  • Jinja2 как шаблонизатор
  • SQLAlchemy как ORM по умолчанию (если что - ее можно заменить)
  • WTForms для управления формами

Svarga представляет собой относительно тонкую прослойку между этими библиотеками. Кроме того, реализует некоторые вещи из django.contrib, например django.contrib.auth.

Разработка ведется в фоновом режиме, так что фреймворк скорее хобби, чем основная работа. Первые строчки кода попали в репозиторий примерно год назад.

Теперь немного о том, как это все выглядит.

Некоторые архитектурные решения могут показаться спорными, но я попробую объяснить чем они вызваны и какие они могут иметь последствия.

Environment

В Svarga введено понятие окружения. Окружение это “глобальный” объект, который содержит всякие разные настройки, необходимые для обслуживания запроса от клиента. Каждый запрос получает свое личное окружение.

Глобальность окружения условная - окружение это что-то типа threadlocals объекта с поддержкой greenlet’ов, и состоит в том, что оно доступно отовсюду.

Делается это как-то так:

from svarga import env

Конечно, таким подходом теряется функциональная чистота кода и, теоретически, производительность. Производительность теряется в том, что обращение к атрибутам окружения идет через промежуточный метод.

Кроме того, в тех приложениях что я видел в Django, почти всегда кто-то пишет свой велосипед для доступа к request через threadlocals. В Svarga вы можете хранить переменные в окружении.

Что хранится в environment? Куча всего, например:

  • env.request - текущий запрос, если есть
  • env.sqla - сессия алхимии, если включена
  • env.jinja - контекст Jinja2
  • env.user - если подключен contrib.auth
  • и т.д.

Следует заметить, что окружение доступно из шаблонов:

{{ env.user.last_name }}, {{ env.user.first_name }}

Что у нас получилось с таким подходом? Примерно следующее:

  • Окружение доступно отовсюду, что само по себе большой плюс
  • Решение вопроса с загрязнением объекта запроса. Лично я считаю, что кормить дополнительные поля в request в Django это неправильно
  • Унификация хранения различных данных, которые необходимы для работы программы

Какие последствия использования окружения? Самое главное - во view не передается request, так как он и так доступен через окружение. Вот именно тут теряются функциональные принципы: функция которая, возможно, работает с запросом, не принимает его как параметр. Однако, благодаря Werkzeug этот самый запрос не всегда и нужен, так как следующий код это вполне себе полноценный view:

@as_html('hello.html')
def hello(a,b,c,d):
    return dict(m=a*b*c*d)

Дело в том, что Werkzeug умеет проверять, конвертировать и передавать параметры во view. Правило для примера выше выглядеть как-то так:

Rule('/mul/<int:a>/<int:b>/<int:c>/<int:d>/', view='test.views.hello', endpoint='test.hello')

Routing

Routing у нас от Werkzeug. Отличается он от Django тем, что вместо сырых regexp в правилах, используется более-менее читабельный синтаксис.

Кроме того, он поддерживает фильтры, которые проверяют и конвертируют значения. В примере выше, “<int:a>” проверяет что на этом месте будет число и если это число, конвертирует значение в тип int и передает его во view.

Фильтров разных много, и если надо - можно писать свои.

Кроме того, Svarga реализует несколько полезных штук, например так называемые bundle. Bundle (хм, наверное правильно будет его перевести как упаковка) это класс, который содержит view как методы, и является чем-то типа Include() правила.

Т.е. такая вот штука это и есть bundle:

class MyBundle(Bundle):
     @expose('/')
     @as_html('entry.html')
     def entry(self):
         return dict(a=10)

И потом его можно использовать так:

url_map = Map(
    MyBundle('b1', '/1/'),
    MyBundle('b2', '/2/'),
    MyBundle('b3', '/3/'),
)

Обратившись по адресу /1/ получим вызов в первом объекте, /2/ - во втором и так далее.

Обработка запросов

Мы поддерживаем только WSGI интерфейс. На текущий момент, для отладочного режима используется Werkzeug. Но, к сожалению, он медленнее чем CherryPy, так что для production лучше его не использовать. Кроме того, у нас в contrib есть поддержка tornado, который работает быстрее первых двух.

Если кто не знает что такое Werkzeug - советую почитать его документацию. Самое удобное и полезное что в нем есть, это онлайновый отладчик. Упало приложение? Не вопрос, можно на месте посмотреть stack trace, а заодно и получить доступ к консоли и посмотреть какие были переменные, потрейсить и т.д. По сути, это полноценный pdb, но через браузер.

Как и в джанго, у нас есть request middleware, причем подход ничем не отличается - можно получить управление до выполнения запроса, можно после, можно при исключении и т.д.

Приложения

Система приложений работает полностью аналогично оной в Django. Т.е. функционал всего проекта разбивается на отдельные части, которые можно подключать. Основные отличия:

  • Возможность инициализации приложения при старте процесса
  • Поддержка локальных директорий с шаблонами
  • Поддержка локальных директорий со статическими файлами. Причем есть команда генерации конфигурации для apache/nginx

Про первое хотелось бы рассказать немного больше. Каждое приложение может представить такую вот функцию в своем __init__.py:

def init(settings, env_class):
    settings.add_template_path('blog', 'templates')
    settings.add_static_dir('blog', 'static')

Эта функция вызовется один раз и позволит выполнить некий код инициализации приложения.

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

Чем это удобно?

  1. Возможность конфигурировать приложения так, как ему надо
  2. Общий подход для всех приложений - никто не будет делать какие-то там синглтоны и флажки, что приложение уже инициализировано
  3. Возможность расширить класс-окружения, вместо постоянного добавления свойств уже во время жизни приложения. Например, в Django, request.auth добавляется в request middleware, на каждый запрос. У нас - один раз, при инициализации contrib.auth. Удобно и быстро.

Модели

Изначально, мы поддерживали только SQLAlchemy, но потом решили что можно поддержать GAE, а может и вообще nosql базы данных. В результате чего, код был немного отрефакторен. Сейчас имеется следующее:

  • Возможность добавления бекендов для разных библиотек
  • Возможность использования нескольких разных бекендов параллельно
  • Определен некий общий знаменатель для декларативного синтаксиса описания моделей. Если модель описать с помощью этого синтаксиса, бекенд БД конвертирует такую модель в понятное его библиотеки представление. Дает возможность писать один код для разных баз данных, например общий код contrib.auth отлично работает и под SQLAlchemy и под GAE.
  • Общий формат метаданных, который создает конкретный бекенд. Т.е. можно работать с описанием модели через общий интерфейс. Сейчас ModelForm и ModelFormSet используют метаданные для создания форм.
  • Определен общий интерфейс ModelManager, по аналогии с Django.

По умолчанию, используется SQLAlchemy. Основа моделей - sqlalchemy.ext.declarative, с кучкой всяких полезностей и красивостей.

Несколько примеров красивостей:

  • Автоматическая генерация primary key с названием id, если его нет
  • Автоматическая генерация имени таблицы, если ее нет
  • Всякие хелперы для создания ManyToMany связей без ручного создания промежуточной таблицы

В целом, работа с моделями не сильно отличается от работы с моделями в Django, за исключением использования алхимии и ее синтаксиса работы с запросами. К слову, алхимия в этом плане наголову выше чем Django. Мне даже для очень сложных запросов не пришлось писать сырой SQL.

Шаблоны

Для шаблонизации мы используем Jinja 2. Сама по себе Jinja крута:

  • Очень-очень быстрая (шаблоны компилируются в код питона)
  • Поддерживает sandboxing, особенно для случаев, когда шаблоны могут храниться в базе
  • Умеет сильно больше чем шаблоны Django. Писать свои теги на каждый чих больше не надо
  • Синтаксис от Django

Единственная вещь, которую ставят в недостаток Jinja - возможность писать логику в шаблонах. Тут я категорически не согласен. Следует разделять логику приложения и логику представления. Генерация меню по определенным правилам - это точно логика представления, но в Django придется писать своей тег, если там не просто цикл по массиву. В Jinja это решается встроенными средствами.

Команды

Писать свои команды в джанго было сущим наказанием. Для добавления команды приходилось писать страницу кода. Данная секция является больше рекламой Opster‘a (тоже от Пираньи), который занимается менеджментом команд и генерацией документации для них.

Вот пример вполне полноценной команды:

@command(name='lock-users', usage='-l LOCK users')
def lock_users(users, lock=('l', True, 'true or false')):
    'Lock or unlock users'
    q = User.objects.filter(User.name.contains(name))
    q = q.update(dict(locked=lock))
    print 'Done'

Кстати, благодаря алхимии, в коде выше, sql injection не произойдет. Запрос будет вида: UPDATE users SET locked=? WHERE users.name LIKE ? и все литералы и параметры будут переданы отдельно.

Админка

Django не была бы такой без своей знаменитой админки. Тут мы абсолютно согласны с большинством. Но… Лично я считаю что админка в Django сделана плохо - она нерасширяемая. Точнее, количество телодвижений, необходимых для изменения ее функционала, слишком велико даже для простых вещей.

В результате, было принято решение пойти по другому пути. Админка в Svarga это некий микро-фреймворк для создания административного интерфейса. Она легко расширяется - можно легко добавить нестандартные вьюшки или изменить поведение дефолтных. По сути, админка основана на bundle, которые просто монтируются под /admin/.

Пример “полноценной” админки модели:

class SampleAdmin(ModelAdmin):
    class Meta:
        menu = ('Sample Admin', 'Test')
        model = Sample

Определяем ее в приложении в admin.py и получаем админку модели Sample. Если очень хочется, можно свои вьюшки добавить, если унаследовать класс от AdminView и реализовать свои view методы.

К слову, рабочей и стабильной админки еще нет, она все еще в стадии разработки.

Приложеньица

Фреймворк постоянно развивается. Сейчас в contrib обитает следующее:

  • admin - микро-фреймворк для построения админки. Все еще в разработке, но посмотреть уже можно.
  • appengine - базовая поддержка Google AppEngine. К сожалению не развивается и не умеет метаданных (ModelForm не взлетит), но работает. Patches are welcome, as usual.
  • auth - аналог django.contrib.auth. Умеет много всего, например возможность переопределения модели User, более чистое API - данные отделены от логики, поддержка нескольких backend’ов и т.д.
  • cache - аналог django.contrib.cache
  • cherrypy - HTTP сервер с использованием CherryPy
  • dberrorlog - простенький logger всяких эксепшенов в базу
  • jslib - всякие полезности и удобности при работе с JavaScript
  • migrate - обертка над SQLAlchemy-migrate, с удобностями
  • sessions - аналог django.contrib.session
  • tableview - позволяет строить HTML таблички, который умеет сортировку, paging из набора исходных данных
  • tornado - HTTP сервер с использованием Tornado

Что дальше?

Следует учесть, что на текущий момент, это хобби. И поскольку это хобби, проектом занимаемся когда есть свободное время.

У проекта серьезно хромает документация - ее почти нет и очень сильно не хватает unit test’ов.

На текущий момент, занимаемся:

  • Чисткой и украшательством API
  • Доделываем админку
  • Доделываем миграции (улучшение интеграции с sqlalchemy-migrate)
  • Поддержкой интернационализации

Напоследок хочется сказать - patches and contributions are welcome :-)

P.S. Забыл самое главное, проект живет тут: http://bitbucket.org/piranha/svarga/wiki/Home

blog comments powered by Disqus