05 июля 2012

Структура Pyramid приложений как в Django

Одной из причин отказа развивать ветку Pylons стала его архитектура проекта. Все контроллеры хранятся в директории controllers, модели в models, шаблоны в templates. Это очень удобно когда у вас маленький проект, но если он разрастается до десятков и сотен сущностей, то становится крайне сложно скакать по этим папкам выискивая нужный файл, относящийся именно к этой сущности. В Django сделано по другому, в проекте хранятся приложения(application) - это такие маленькие подпрограммы которые отвечают за конкретный функционал проекта(например фотогалерея django-photologue или дерево сайта django-sitetree и прочее). Каждое такое приложение имеет свою папку и уже в ней хранятся контроллеры(views в данном случае) и модели(models). Т.е. вместо такой архитектуры Pylons:
Project/
    controllers/
        controllers1.py
        controllers2.py
    model/
        models1.py
        models2.py
    templates/
        templates1/
        templates2/
    routes.py
мы получаем
Project/
    app1/
        models.py
        routes.py
        views.py
    app2/
        models.py
        routes.py
        views.py
    templates/
        templates1/
        templates2/

Разработчики не стали менять архитектуру Pylons и создавать Pylons 2.0, а начали развивать новый проект Pyramid с похожей на Django структурой проекта. При этом Pylons не считается устаревшим, а просто имеет немного другой подход к разработке.

Стоит ли сейчас выбирать Pylons? Да, если вы его хорошо знаете и не планируете создавать слишком масштабное приложение, иначе лучше все таки выбрать Pyramid.
Посмотрим как в нем организовать структуру проекта:

После создания проект выглядит так
MyProject/
├── CHANGES.txt
├── development.ini
├── MANIFEST.in
├── myproject
│   ├── __init__.py
│   ├── models.py
│   ├── scripts/
│   ├── static/
│   ├── templates/
│   ├── tests.py
│   └── views.py
├── production.ini
├── README.txt
├── setup.cfg
└── setup.py

Наша задача перенести models.py, views.py, tests.py в app1. Создаем папку app1 и переносим файлы.
Должно получится так:
MyProject/
.
├── CHANGES.txt
├── development.ini
├── MANIFEST.in
├── myproject
│   ├── app1
│   │   ├── __init__.py
│   │   ├── models.py
│   │   ├── tests.py
│   │   └── views.py
│   ├── __init__.py
│   ├── scripts
│   │   ├── initializedb.py
│   │   └── __init__.py
│   ├── static
│   │   ├── favicon.ico
│   │   ├── footerbg.png
│   │   ├── headerbg.png
│   │   ├── ie6.css
│   │   ├── middlebg.png
│   │   ├── pylons.css
│   │   ├── pyramid.png
│   │   ├── pyramid-small.png
│   │   └── transparent.gif
│   └── templates
│       └── mytemplate.pt
├── production.ini
├── README.txt
├── setup.cfg
└── setup.py

Меняем __init__.py в проекте:

Вместо
from pyramid.config import Configurator
from sqlalchemy import engine_from_config

from .models import DBSession

def main(global_config, **settings):
    """ This function returns a Pyramid WSGI application.
    """
    engine = engine_from_config(settings, 'sqlalchemy.')
    DBSession.configure(bind=engine)
    config = Configurator(settings=settings)
    config.add_static_view('static', 'static', cache_max_age=3600)
    config.add_route('home', '/')
    config.scan()
    return config.make_wsgi_app()

Делаем
from pyramid.config import Configurator
from sqlalchemy import engine_from_config

from .models import DBSession

def main(global_config, **settings):
    """ This function returns a Pyramid WSGI application.
    """
    engine = engine_from_config(settings, 'sqlalchemy.')
    DBSession.configure(bind=engine)
    config = Configurator(settings=settings)
    config.add_static_view('static', 'static', cache_max_age=3600)

    # add config for each of your subapps
    config.include('myproject.app1')

    # pyramid_jinja2 configuration
    config.include('pyramid_jinja2')
    config.add_jinja2_search_path("myproject:templates")

    return config.make_wsgi_app()

Теперь в этом __init__.py мы будем хранить глобальные настройки, а в инитах апликайшинах настройки самих апликайшинов.

Добавляем models.py в основной проект
MyProject/
.
├── CHANGES.txt
├── development.ini
├── MANIFEST.in
├── myproject
│   ├── app1
│   │   ├── __init__.py
│   │   ├── models.py
│   │   ├── tests.py
│   │   └── views.py
│   ├── __init__.py
│   ├── scripts
│   │   ├── initializedb.py
│   │   └── __init__.py
│   ├── models.py
│   ├── static
│   │   ├── favicon.ico
│   │   ├── footerbg.png
│   │   ├── headerbg.png
│   │   ├── ie6.css
│   │   ├── middlebg.png
│   │   ├── pylons.css
│   │   ├── pyramid.png
│   │   ├── pyramid-small.png
│   │   └── transparent.gif
│   └── templates
│       └── mytemplate.pt
├── production.ini
├── README.txt
├── setup.cfg
└── setup.py

В нем будет хранится сессия SQLAlchemy общая для всех проектов
from zope.sqlalchemy import ZopeTransactionExtension
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import (
    scoped_session,
    sessionmaker,
    )

DBSession = scoped_session(sessionmaker(extension=ZopeTransactionExtension()))
Base = declarative_base()

Меняем __init__.py в app1
def includeme(config):
    config.add_route('home', '/')
    config.scan()

Меняем models.py в app1
from sqlalchemy import (
    Column,
    Integer,
    Text,
    )

from sqlalchemy.ext.declarative import declarative_base

from sqlalchemy.orm import (
    scoped_session,
    sessionmaker,
    )

from ..models import (
    DBSession,
    Base,
    )

class MyModel(Base):
    __tablename__ = 'models'
    id = Column(Integer, primary_key=True)
    name = Column(Text, unique=True)
    value = Column(Integer)

    def __init__(self, name, value):
        self.name = name
        self.value = value


Меняем tests.py
import unittest
import transaction

from pyramid import testing

from ..models import DBSession

class TestMyView(unittest.TestCase):
    def setUp(self):
        self.config = testing.setUp()
        from sqlalchemy import create_engine
        engine = create_engine('sqlite://')
        from .models import (
            Base,
            MyModel,
            )
        DBSession.configure(bind=engine)
        Base.metadata.create_all(engine)
        with transaction.manager:
            model = MyModel(name='one', value=55)
            DBSession.add(model)

    def tearDown(self):
        DBSession.remove()
        testing.tearDown()

    def test_it(self):
        from .views import my_view
        request = testing.DummyRequest()
        info = my_view(request)
        self.assertEqual(info['one'].name, 'one')
        self.assertEqual(info['project'], 'MyProject')

Меняем views.py
from sqlalchemy.exc import DBAPIError

from ..models import DBSession
from .models import MyModel

@view_config(route_name='home', renderer='../templates/mytemplate.pt')
def my_view(request):
    try:
        one = DBSession.query(MyModel).filter(MyModel.name=='one').first()
    except DBAPIError:
        return Response(conn_err_msg, content_type='text/plain', status_int=500)
    return {'one':one, 'project':'MyProject'}

conn_err_msg = """\
Pyramid is having a problem using your SQL database.  The problem
might be caused by one of the following things:

1.  You may need to run the "initialize_MyProject_db" script
    to initialize your database tables.  Check your virtual 
    environment's "bin" directory for this script and try to run it.

2.  Your database server may not be running.  Check that the
    database server referred to by the "sqlalchemy.url" setting in
    your "development.ini" file is running.

After you fix the problem, please restart the Pyramid application to
try it again.
"""


Меняем initializedb.py в scripts
import os
import sys
import transaction

from sqlalchemy import engine_from_config

from pyramid.paster import (
    get_appsettings,
    setup_logging,
    )

from ..models import (
    DBSession,
    Base,
    )

from ..app1.models import MyModel

def usage(argv):
    cmd = os.path.basename(argv[0])
    print('usage: %s \n'
          '(example: "%s development.ini")' % (cmd, cmd)) 
    sys.exit(1)

def main(argv=sys.argv):
    if len(argv) != 2:
        usage(argv)
    config_uri = argv[1]
    setup_logging(config_uri)
    settings = get_appsettings(config_uri)
    engine = engine_from_config(settings, 'sqlalchemy.')
    DBSession.configure(bind=engine)
    Base.metadata.create_all(engine)
    with transaction.manager:
        model = MyModel(name='one', value=1)
        DBSession.add(model)

Выполняем python setup.py install, запускаем проект, профит!
Остальные апликайшины добавляются по аналогии.
Навеянно этим http://stackoverflow.com/questions/6012991/pyramid-project-structure

5 комментариев:

  1. со статикой чегонтьбы придумать.
    в джанге очень удачно сделано.

    ОтветитьУдалить
    Ответы
    1. Может я конечно не все знаю про Джангу но в по моему в Pyramid это делается аналогично config.add_static_view('static', 'static', cache_max_age=3600)

      Здесь все описано: http://pyramid.readthedocs.org/en/latest/narr/assets.html

      Для деплоя можно отдавать через nginx например.

      Удалить
    2. так, да не так :)
      или я чтото не доконца осилил.

      в джанге в каждом app своя статика.
      и она потом радостно склеивается в одну (и простой командой деплоится куда скажешь, в т.ч. симлинками). хотя эта красивость не так давно появилась в полном объеме(чуть ли не в 1.4).

      Удалить
    3. Последнюю джангу которую я использовал это была 0.96 версия поэтому мне сложно представить что за магия используется там сейчас в статике. Если я правильно понял задачу, то вам нужно в pyramid подключить какое то приложение к своему и сделать так что бы статика читалась из подключенного и из вашего проекта.

      Для подключения проекта можно покурить статью:
      http://docs.pylonsproject.org/projects/pyramid/en/1.0-branch/narr/extending.html

      Например в includeme подключаемого проекта pyramid_A:

      def includeme(config):
      ....config.include('pyramid_jinja2')
      ....config.add_static_view('static_A', 'pyramid_A:static')
      ....config.add_translation_dirs("pyramid_A:locale/")
      ....config.include(add_routes)
      ....config.scan()

      В вашем проекте pyramid_B:

      config.include("pyramid_A")
      config.add_jinja2_search_path(("pyramid_B:templates", "pyramid_A:templates"))
      config.add_static_view('static_B', 'pyramid_B:static')

      Теперь в шаблонах просто пишите путь /static_A/css/blablabla.css или /static_B/img/blablabla.img

      В принципе в документации более подробно описан метод подключения нескольких директорий статики: http://pyramid.readthedocs.org/en/latest/narr/assets.html#generating-static-asset-urls

      Удалить
    4. Примерно это. просто хочется немного автомотизировать чтоли и деплой прикрутить

      Удалить