Показаны сообщения с ярлыком SQLalchemy. Показать все сообщения
Показаны сообщения с ярлыком SQLalchemy. Показать все сообщения

12 мая 2014

MPTT для SQLAlchemy

Запилил я тут для своих нужд небольшое приложение которое добавляет в модель поля и функционал необходимый для Nested sets. По аналогии с django-mptt. Грубо говоря в модель добавляются поля left и right которые при помощи системы эвентов самостоятельно пересчитываются при изменении дерева. Ниже пример обхода дерева:
 Простой пример:


from sqlalchemy import Column, Integer, Boolean
from sqlalchemy.ext.declarative import declarative_base

from sqlalchemy_mptt.mixins import BaseNestedSets

Base = declarative_base()


class Tree(Base, BaseNestedSets):
    __tablename__ = "tree"

    id = Column(Integer, primary_key=True)
    visible = Column(Boolean)  # Наше кастомное поле

    def __repr__(self):
        return "" % self.id

Tree.register_tree()  # Регистрирует event'ы

Теперь попробуем добавить ноду:

node = Tree(parent_id=6)
session.add(node)
получим
        level           Nested sets example
        1                    1(1)22
                _______________|___________________
               |               |                   |
        2    2(2)5           6(4)11             12(7)21
               |               ^                   ^
        3    3(3)4       7(5)8   9(6)10    13(8)16   17(10)20
                                              |          |
        4                                  14(9)15   18(11)19

        level     Insert node with parent_id == 6
        1                    1(1)24
                _______________|_________________
               |               |                 |
        2    2(2)5           6(4)13           14(7)23
               |           ____|____          ___|____
               |          |         |        |        |
        3    3(3)4      7(5)8    9(6)12  15(8)18   19(10)22
                                   |        |         |
        4                      10(23)11  16(9)17  20(11)21

Удаление:
node = session.query(Tree).filter(Tree.id == 4).one()
session.delete(node)
получим
        level           Nested sets example
        1                    1(1)22
                _______________|___________________
               |               |                   |
        2    2(2)5           6(4)11             12(7)21
               |               ^                   ^
        3    3(3)4       7(5)8   9(6)10    13(8)16   17(10)20
                                              |          |
        4                                  14(9)15   18(11)19

        level         Delete node == 4
        1                    1(1)16
                _______________|_____
               |                     |
        2    2(2)5                 6(7)15
               |                     ^
        3    3(3)4            7(8)10   11(10)14
                                |          |
        4                     8(9)9    12(11)13

Обновление:
node = session.query(Tree).filter(Tree.id == 8).one()
node.parent_id = 5
session.add(node)
получим
        level           Nested sets example
            1                    1(1)22
                    _______________|___________________
                   |               |                   |
            2    2(2)5           6(4)11             12(7)21
                   |               ^                   ^
            3    3(3)4       7(5)8   9(6)10    13(8)16   17(10)20
                                                  |          |
            4                                  14(9)15   18(11)19

        level               Move 8 - > 5
            1                     1(1)22
                     _______________|__________________
                    |               |                  |
            2     2(2)5           6(4)15            16(7)21
                    |               ^                  |
            3     3(3)4      7(5)12   13(6)14      17(10)20
                               |                        |
            4                8(8)11                18(11)19
                               |
            5                9(9)10
Перенос ноды по дереву или между деревьев:

Move inside (перемещает ноду на первое место от родителя):
node = session.query(Tree).filter(Tree.id == 8).one()
node = session.query(Tree).filter(Tree.id == 4).one()
node.move_inside("15")
получим
                 4 -> 15
        level           Nested sets tree1
        1                    1(1)16
                _______________|_____________________
               |                                     |
        2    2(2)5                                 6(7)15
               |                                     ^
        3    3(3)4                            7(8)10   11(10)14
                                                |          |
        4                                     8(9)9    12(11)13

        level           Nested sets tree2
        1                     1(12)28
                 ________________|_______________________
                |                |                       |
        2    2(13)5            6(15)17                18(18)27
               |                 ^                        ^
        3    3(14)4    7(4)12 13(16)14  15(17)16  19(19)22  23(21)26
                         ^                            |         |
        4          8(5)9  10(6)11                 20(20)21  24(22)25
Move after (перемещает ноду после текущей ноды):
node = session.query(Tree).filter(Tree.id == 8).one()
node = session.query(Tree).filter(Tree.id == 8).one()
node.move_after("5")
получим
       level           Nested sets example
            1                    1(1)22
                    _______________|___________________
                   |               |                   |
            2    2(2)5           6(4)11             12(7)21
                   |               ^                   ^
            3    3(3)4       7(5)8   9(6)10    13(8)16   17(10)20
                                                  |          |
            4                                  14(9)15   18(11)19

        level               Move 8 after 5
            1                     1(1)22
                     _______________|__________________
                    |               |                  |
            2     2(2)5           6(4)15            16(7)21
                    |               ^                  |
            3     3(3)4    7(5)8  9(8)12  13(6)14   17(10)20
                                    |                  |
            4                    10(9)11            18(11)19
Move to top level (выделение ноды в самостоятельное дерево):
node = session.query(Tree).filter(Tree.id == 8).one()
node = session.query(Tree).filter(Tree.id == 15).one()
node.move_after("1")
получим
        level           tree_id = 1
        1                    1(1)22
                _______________|___________________
               |               |                   |
        2    2(2)5           6(4)11             12(7)21
               |               ^                   ^
        3    3(3)4       7(5)8   9(6)10    13(8)16   17(10)20
                                              |          |
        4                                  14(9)15   18(11)19

        level           tree_id = 2
        1                     1(15)6
                                 ^
        2                 2(16)3   4(17)5

        level           tree_id = 3
        1                    1(12)16
                 _______________|
                |               |
        2    2(13)5          6(18)15
                |               ^
        3    3(14)4     7(19)10   11(21)14
                           |          |
        4               8(20)9    12(22)13

За основу был взят пример Mike Bayer, впринципе в тестах можно посмотреть больше примеров. Ссылка на github: https://github.com/ITCase/sqlalchemy_mptt

15 марта 2014

Перенос БД с sqlite на postgres

ORM позволяет быстро переключатся между БД не учитывая их диалект(практически). Но данные хранятся физически в разных местах и естественно их надо переносить, например при переключении с sqlite на PostgreSQL. В Django есть встроенный функционал в виде:

# Выгрузка в JSON
python manage.py dumpdata myapp.A > a.json

# Загрузка из JSON
python manage.py loaddata a.json

Т.е. мы выгружаем данные из sqlite в JSON формат, затем меняем строку подключения на postgres и выполняем загрузку из JSON. Очень удобно, но почему то этот метод не работает, либо работает только при переносе из sqlite -> sqlite, что в принципе не очень интересно, точнее бессмысленно. Есть какие то решений с бубном как это вот http://macrotoma.blogspot.ru/2012/10/solved-move-django-from-sqlite-to-mysql.html, http://blog.abourget.net/2009/7/7/exporting-sql-schemas-from-sqlalchemy-table-definitions/.

Эти методы не универсальны, потому что имеют привязку к моделям(ORM), требуют для переноса проект на Django и ручные действия вроде создания схемы и выполнения миграций(далеко не всегда миграции созданы правильно).

Я написал небольшой пример как можно перевести данные не имея фреймворков, не привязываясь к моделям, указав только две строки подключения откуда переносить и куда(вроде было что то похожее на руби). За основы взят пример из этой статьи http://www.tylerlesmann.com/2009/apr/27/copying-databases-across-platforms-sqlalchemy/. Где предлагается указать дополнительно названия таблиц.
В sqlalchemy с весии 9.1 появилась встроенная возможность автоматического определения схемы БД http://docs.sqlalchemy.org/en/rel_0_9/orm/extensions/automap.html. Правда намного раньше появились сторонние решения: http://turbogears.org/2.1/docs/main/Utilities/sqlautocode.html или https://sqlsoup.readthedocs.org/en/latest/.

Первое что мы сделаем, это получим схему БД
    
from sqlalchemy.ext.automap import automap_base

def get_metadata(self, engine):
   # produce our own MetaData object
   metadata = MetaData()

   # we can reflect it ourselves from a database, using options
   # such as 'only' to limit what tables we look at...
   # only = ['news_news', 'pages_page']
   if self.only:
       metadata.reflect(engine, only=self.only)
   else:
       metadata.reflect(engine)

   # we can then produce a set of mappings from this MetaData.
   Base = automap_base(metadata=metadata)

   # calling prepare() just sets up mapped classes and relationships.
   Base.prepare()
   return metadata, Base


Дальше получаем все таблицы
tables = Base.classes


Создаем такую же структуру БД в postgres:
metadata.create_all(self.engine_dst)


По очереди проходим каждую таблицу и переносим из нее данные в новую БД
for table in tables:
    columns = table.__table__.c.keys()
    print 'Transferring records to %s' % table.__table__.name
    for record in self.session.query(table).all():
        data = dict(
            [(str(column), getattr(record, column)) for column in columns]
        )
        NewRecord = quick_mapper(table.__table__)
        self.session_dst.merge(NewRecord(**data))
        self.session_dst.commit()


Если возникли конфликты можно их решить переопределением типов полей. Например при переносе из sqlite в postgres тип полей DATETIME нужно заменить на DateTime
dialect = self.engine.dialect.name
dialect_dst = self.engine_dst.dialect.name
if dialect == dialect_dst:
    return
for table in self.tables:
    columns = table.__table__.c
    for column in columns:
        if dialect_dst == 'postgresql':
            # DATETIME->DateTime
            if isinstance(column.type, DATETIME):
                column.type = DateTime()


 Пример запуска:
python convertdbdata.py -f "sqlite:///fromMydb.sqlite"
-t "postgresql://postgres:postgres@localhost/toMydb" -i "auth_user,news_news"

 -i параметр указывает какие таблицы нужно запускать первыми, например в такой ситуации:
    
DETAIL:  Ключ (user_id)=(1) отсутствует в таблице "auth_user".
 'INSERT INTO django_admin_log (id, action_time, user_id, content_type_id, object_id, object_repr, action_flag,
 change_message) VALUES (%(id)s, %(action_time)s, %(user_id)s, %(content_type_id)s, %(object_id)s, %(object_repr)s,
 %(action_flag)s, %(change_message)s)' {'action_flag': 1, 'action_time': datetime.datetime(2014, 2, 5, 13, 15, 27, 948000),
 'user_id': 1, 'content_type_id': 39, 'object_repr': u'dfgsdfg', 'object_id': u'1', 'change_message': u'', 'id': 1}

Код полностью https://github.com/ITCase/convertdbdata 

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

10 марта 2014

Обновление sacrud. Версия 0.1.2

Код: https://github.com/uralbash/sacrud
Описание: http://sacrud.readthedocs.org/ (в процессе)

В этой версии делался упор на кастомизацию интерфейса.

Что нового?
* добавлена пагинация
* теперь pk показывается по умолчанию в форме создания/редактирования
* новая опция sacrud_detail_col где можно задать отображаемые поля в форме редактирования
* новая опция sacrud_list_col где можно задать отображаемые поля в списке записей
* новая опция verbose_name для полей и таблиц
* опция sacrud_css_class, назначает CSS стили полям
* новый атрибут колонки sacrud_position: "inline" (см. реализацию horizontal_fields)
* новая функция horizontal_fields
* новый тип exttype.GUID
* для переопределения base.html создан шаблон redefineme.html
* вывод флеш уведомлений если задана sesion_factory
* исправлены названия классов в шаблонах у полей
* исправлен шаблон ForeignKey.jinja2
* sa_create, sa_read, sa_update, sa_delete view вынесены в общий класс CRUD
* в example добавлены pyramid_beaker , примеры с кастомизацией, FileField и все остальные поля которые есть в sacrud/templates/sacrud/types.

 Пример кастомизации:
models.py

class TestCustomizing(Base):
    __tablename__ = "test_customizing"

    id = Column(Integer, primary_key=True)
    name = Column(String)
    date = Column(Date, info={"verbose_name": 'date JQuery-ui'})
    name_ru = Column(String, info={"verbose_name": u'Название', })
    name_fr = Column(String, info={"verbose_name": u'nom', })
    name_bg = Column(String, info={"verbose_name": u'Име', })
    name_cze = Column(String, info={"verbose_name": u'název', })
    description = Column(Text)
    description2 = Column(Text)

    visible = Column(Boolean)
    in_menu = Column(Boolean, info={"verbose_name": u'menu?', })
    in_banner = Column(Boolean, info={"verbose_name": u'on banner?', })

    # SACRUD
    verbose_name = u'Customizing table'
    sacrud_css_class = {'tinymce': [description, description2],
                        'content': [description],
                        'name': [name], 'Date': [date]}
    sacrud_list_col = [name, name_ru, name_cze]
    sacrud_detail_col = [name,
                         hosrizontal_field(name_ru, name_bg, name_fr, name_cze,
                                           sacrud_name=u"i18n names"),
                         description, date,
                         hosrizontal_field(in_menu, visible, in_banner,
                                           sacrud_name=u"Расположение"),
                         description2]

templates/sacrud/redefineme.jinja2
{% extends "sacrud/base.jinja2" %}

{% block userspace %}
  {{ super() }}

  <!-- Date field -->
  <script src="http://code.jquery.com/jquery-1.10.2.js"></script>
  <script src="http://code.jquery.com/ui/1.10.4/jquery-ui.js"></script>

  <link href="//code.jquery.com/ui/1.10.4/themes/smoothness/jquery-ui.css" rel="stylesheet"></link>
  <style>
    .content {
      height: 550px;
    }
    .name {
      width: 400px;
    }
  </style>

  <script>
    $(function() {
      $(".Date").datepicker({ dateFormat: 'yy-mm-dd' });
    });
  </script>

  <script src="//tinymce.cachefly.net/4.0/tinymce.min.js"></script>
  <script>
    tinymce.init({
      selector:'textarea.tinymce',
      plugins: "image link",
      file_browser_callback: function(field_name, url, type, win) {
        tinymce.activeEditor.windowManager.open({
            title: "SACRUD file browser",
            url: "/image/filebrowser",
            width: 600,
            height: 400,
          }, {
            oninsert: function(url) {
              win.document.getElementById(field_name).value = url;
            }
        });
      },
    });
  </script>
{% endblock %}

{% block sa_body %}
  {{ super() }}
{% endblock %}

Результат:
Пример кастомного CRUD интерфейса для SQLAlchemy

28 февраля 2014

Своя панель в pyramid_debugtoolbar

В pyramid_debugtoolbar можно создавать панели для своих нужд.
Документация здесь: http://docs.pylonsproject.org/projects/pyramid_debugtoolbar/en/latest/#adding-custom-panels

Я создал небольшой пример, как это сделать на примере sadisplay.
sadisplay - это модуль который отображает модели SQLAlchemy в виде UML диаграммы.



Было бы удобно видеть схему БД проекта в дебаг панеле. Для этого создадим папку проекта pyramid_debugtoolbar_sadisplay со структурой:

├── __init__.py
├── panel.py
└── templates
    └── base.dbtmako

panel.py
#! /usr/bin/env python
# -*- coding: utf-8 -*-
# vim:fenc=utf-8
#
# Copyright © 2014 uralbash 
#
# Distributed under terms of the MIT license.

"""
sadisplay in pyramid_debugtoolbar
"""
import pydot
import sadisplay
import sqlalchemy

from sqlalchemy import engine_from_config
from pyramid_debugtoolbar.panels import DebugPanel

_ = lambda x: x


def get_sa_base(request, settings=None):
    if settings is None:
        settings = request.registry.settings
    engine = engine_from_config(settings, 'sqlalchemy.')
    sabase = sqlalchemy.ext.declarative.declarative_base()
    sabase.metadata.reflect(engine)
    return sabase


class SadisplayDebugPanel(DebugPanel):
    """
debug panel
"""
    name = 'SADisplay'
    has_content = True
    template = 'pyramid_debugtoolbar_sadisplay:templates/base.dbtmako'

    def __init__(self, request):
        self.request = request
        self.data = {}
        self.Base = get_sa_base(request)

    def nav_title(self):
        return _('SADisplay')

    def url(self):
        return ''

    def title(self):
        return _('SADisplay')

    def render_vars(self, request):
        tables = self.Base.metadata.tables.values()
        desc = sadisplay.describe(tables)
        dot_data = sadisplay.dot(desc)
        graph = pydot.graph_from_dot_data(str(dot_data))
        svg_img = graph.create_svg()
        return {'svg_img': svg_img}

    def content(self):
        vars = self.render_vars(self.request)
        return self.render(
            'pyramid_debugtoolbar_sadisplay:templates/base.dbtmako',
            vars, self.request)

Функция get_sa_base создает base объект и заполняет его метаданными по строке подключения к БД, в дальнейшем мы сможем получить все модели проекта.
Метод render_vars при помощи sadisplay генерит текст формата dot и при помощи pydot конвертит его в svg.

В версии pyramid_debugtoolbar 1.0.* нужно рендерить шаблон в методе content. В 2.0.* метод content не нужен, шаблон указывается в виде атрибута template, а параметры атдаются в методе render_vars. В примере используются оба метода, что бы работало во всех версиях.

шаблон base.dbtmako
<h4>
SQLAlchemy models preview</h4>
${ svg_img|n }
link: <a href="https://github.com/uralbash/pyramid_debugtoolbar_sadisplay" style="color: blue;">pyramid_debugtoolbar_sadisplay</a>

Подключаем к приложению:
# __init__.py
# SADisplay in pyramid_debugtoolbar
from pyramid_debugtoolbar_sadisplay.panel import SadisplayDebugPanel
config.registry.settings['debugtoolbar.panels'].append(SadisplayDebugPanel)

Результат:



Код примера здесь: https://github.com/uralbash/pyramid_debugtoolbar_sadisplay

19 ноября 2013

sacrud 0.1.0

Обновил sacrud до 0.1.0


Изменения:
* исправлено куча ошибок
* добавлена документация

В планах система расширения и кастомизации.

06 сентября 2013

Новое в sacrud 0.1.0a

Теперь sacrud 0.1.0a
Что нового:
* dnd через любое поле
* объединение полей
* js сортировка колонок
Приложение для ознакомления pyramid_sacrud_example
В планах:
* yapsy плагины
* mass delete
* ImageField в pyramid_sacrud_example

09 марта 2013

CRUD интерфейс для SQLAlchemy и подключение к Pyramid

Запилил Yet another CRUD интерфейс для SQLAlchemy. По сути это аналог Django админки или FormAlchemy, но ОЧЕНЬ сильно упрощенный, ничего лишнего. Есть поддержка большинства полей + кастомные поля типа файл(для загрузки файлов, изображений) и GUID. Довольно просто подключить к Pyramid проекту и сразу начать работать по адресу http://localhost:6543/sacrud

Проект доступен на github https://github.com/uralbash/sacrud

В след. релизах планирую добавить новые типы полей, кастомные поля типа tree и btree с AJAX обработкой в интерфейсе, расширение для других фреймворков (например flask), кастомные фильтры, пагинацию итд


Установка

PyPi

pip install sacrud

Из исходников

python setup.py install

 

Пример использования в Pyramid

Add to your project config:
# pyramid_jinja2 configuration
config.include('pyramid_jinja2')
config.add_jinja2_search_path("myprojectname:templates")

from .models import (Model1, Model2, Model3,)
# add sacrud and project models
config.include('sacrud.pyramid_ext')
settings = config.registry.settings
settings['sacrud_models'] = (Model1, Model2, Model3)
go to http://localhost:6543/sacrud

Скриншоты

список таблиц

ScreenShot

список записей в таблице

ScreenShot

редактирование записи

ScreenShot

17 февраля 2013

Использование SQLAlchemy в дополнениях к Pyramid

У меня есть дополнение к pyramid которое я включаю почти в каждый проект при помощи includeme. Приложение это дает мне простой CRUD интерфейс с Jinja шаблонами малой кровью. Что позволяет избавиться от монстра FormAlchemy. Естественно каждый проект имеет свое название поэтому пришлось применить немного магии что бы создать универсальный механизм получения DBSession в своем подключаемом дополнении.

Примерная структура папок дополнения:
pyramid_ext_plugin
|-- __init__.py
`-- action.py

В __init__.py находятся все настройки includeme, а в action.py действия с базой. Для получения DBSession в action.py исправим сначала __init__.py:
import sqlalchemy
import sqlalchemy.orm as orm
from zope.sqlalchemy import ZopeTransactionExtension

DBSession = None


def includeme(config):
    global DBSession
    engine = sqlalchemy.engine_from_config(config.registry.settings)
    if DBSession is None:
        DBSession = orm.scoped_session(
            orm.sessionmaker(extension=ZopeTransactionExtension()))
    DBSession.remove()
    DBSession.configure(bind=engine)

Все Ж) теперь можно в action.py писать так:
from pyramid_ext_pugin import DBSession

11 апреля 2012

SQLAlchemy и большие таблицы (Memory error)

SQLAlchemy в больших таблицах при таком запросе s.query(TableName).all() зависает и выдает ошибку Memory error. Для решения проблемы нужно использовать метод yield_per. Если необходимо еще изменять данные, то нужно делать коммиты каждые N записей.
Пример:
    def fdbarp(self):
        ''' Функция переноса арпов из одной таблицы в другую.
            Около 3млн записей.
        '''
        for i, arp in enumerate(s.query(Arp).yield_per(100)):
            new_arp = newArp()
            new_arp.equipment_id = arp.equipment
            new_arp.mac = arp.mac
            new_arp.ip = arp.ip
            new_arp.first_seen = arp.first_seen
            new_arp.last_seen = arp.last_seen
            new_arp.times = arp.times

            s.add(new_arp)
            if not i%10000:
                s.commit()
                s.close()
            print "arp ", i

        s.commit()

16 ноября 2011

SQLAlchemy почему PostgreSQL?

Потому что я могу делать так:
c.nets = s.query(Net)
ip_type = request.GET.get('ip_type', '')
if ip_type=='4':
    c.nets = s.query(Net).filter("family(cidr)=4")
elif ip_type=='6':
    c.nets = s.query(Net).filter("family(cidr)=6")

c.nets = c.nets.order_by(Net.cidr).all()
Здесь если GET переменная ip_type=4 то выбираются все строки где cidr IPv4, если 6 то все стоки где cidr IPv6, иначе просто отдается все. Для фильтрации используется стандартная функция Postgres family. Пример был так расписан для наглядности, теперь сократим:
c.nets = s.query(Net)
ip_type = request.GET.get('ip_type', '')
if ip_type:
    c.nets = s.query(Net).filter("family(cidr)=%s" % ip_type)

c.nets = c.nets.order_by(Net.cidr).all()
По моему это просто шЫкарно.

13 ноября 2011

Pylons + FormAlchemy REST Controller

Для своих REST контроллеров можно использовать, формы FormAlchemy.
Создаем контроллер:
yourproj% paster restcontroller comment comments
Creating yourproj/yourproj/controllers/comments.py
Creating yourproj/yourproj/tests/functional/test_comments.py
Или если нужно в отдельной директории
yourproj% paster restcontroller admin/tracback admin/trackbacks
Creating yourproj/controllers/admin
Creating yourproj/yourproj/controllers/admin/trackbacks.py
Creating yourproj/yourproj/tests/functional/test_admin_trackbacks.py

В файле нашего REST контроллера добавим
from formalchemy.ext.pylons.controller import RESTController

И в конце файла обернем его так
# wrap with formalchemy RESTController
CommentsController = RESTController(CommentsController, 'comment', 'comments')

Теперь если закомментировать какой-нибудь из стандартных методов в контроллере(index, new, update, delete, show, edit) он будет браться из контроллера FormAlchemy со стандартными формами. Довольно удобно в разработке.

27 октября 2011

Pylons + SQLalchemy расширенная модель (Mixin)

Часто при проектировании структуры БД появляется необходимость повторять одни и те же действия с таблицами. Добавлять одинаковые поля, ссылки, счетчики и т.д. Модели в Pylons(SQLAlchemy), как и в большинстве других фреймворках использующих паттерн MVC, являются классом и соответственно могут быть унаследованы от других классов. Это позволяет нам избежать рутинной работы с повторяющимися действиями.

Все расширения для наших моделей будем добавлять в models/common.py
Создадим базовую модель в которой будет поле id, автоматическая генерация названия таблицы (__tablename__) и метод выбора элемента по id (SELECT * FROM table WHERE id=integer):
class Base(object):
    """Базовая модель. Добавляет во всех наследников поле id и атрибут
    __tablename__ который заполняется автоматически. Имя таблицы берется из
    названия класса и переводится в нижний регистр. Таблица наследник имеет по
    умолчанию название и поле id, сильно облегчая жизнь.
    """

    @declared_attr
    def __tablename__(cls):
        if (has_inherited_table(cls) and
            Tablename not in cls.__bases__):
            return None
        return cls.__name__.lower()

    # Method "byId" for use in code like this:
    #   session.query(Table).byId(5)
    #
    # SQL statement like:
    #   SELECT * FROM Table WHERE id = 5;
    @classmethod
    def byId(cls, id) :
        return Session.query(cls).filter_by(id = id).first()

    id =  Column(Integer, autoincrement=True, primary_key=True)
Метод byId сильно сокращает запись в коде например:
before: Session.query(net).filter_by(id = id).first()
after: net.byId(id)

Теперь создадим нашу модель унаследовав все плюшки с базовой модели:
class Net(Base, DeclarativeBase):
    """Net or subnet."""

    cidr = Column(postgresql.CIDR, index = True)
    description = Column(UnicodeText())

    def __init__(self, cidr=''):
        self.cidr = cidr

    def __repr__(self):
        return "%s" % self.cidr
Наша модель связанна с БД при помощи наследования от DeclarativeBase SQLalchemy, имеет название, поле id и метод byId благодаря наследованию от базовой модели Base из файла common.py.

Для более наглядного примера создадим типовую модель для таблиц которые должны содержать служебную информацию. Модель будет добавлять в другие модели поля:
created_by - кто создал
updated_by - последний кто обновил
created_at - дата создания
updated_at - дата последнего обновления
Поля заполняются автоматически. Кто создал и обновил ссылаются на модель auth.User. Пользователь берется из текущей сессии, при помощи библиотеки lib.auth и метода get_user.Откуда взялась модель User можно узнать из этой статьи Авторизация в Pylons за 5 мин при помощи repoze.what.
class CreatedMixin(object):
    """Абстрактная примесь которая добавляет в другие модели поля:
        created_by - кто создал
        updated_by - последний кто обновил
        created_at - дата создания
        updated_at - дата последнего обновления
    Поля заполняются автоматически. Кто создал и обновил ссылаются на модель
    auth.User. Пользователь берется из текущей сессии, при помощи библиотеки 
    lib.auth и метода get_user
    """

    @declared_attr
    def created_by(cls):
        return Column(Integer, ForeignKey('user.user_id',
                      onupdate="cascade", ondelete="restrict"))

    @declared_attr
    def updated_by(cls):
        return Column(Integer, ForeignKey('user.user_id',
                      onupdate="cascade", ondelete="restrict"))

    created_at = Column(DateTime, nullable=False, default=dt.now())
    updated_at = Column(DateTime, nullable=False, default=dt.now(),
                        onupdate=dt.now())
Внешние ссылки и другие атрибуты отличающиеся от обычных полей нужно добавлять при помощи декоратора declare_attr. Теперь меняем нашу модель Net просто добавив CreateMixin:
class Net(Base, DeclarativeBase, CreatedMixin):

Вот полный листинг common.py:
# coding=utf-8
"""Модуль с типовыми моделями
"""

from sqlalchemy import Column, ForeignKey
from sqlalchemy.orm import relation, relationship
from sqlalchemy.types import Integer, String, DateTime
from sqlalchemy.ext.declarative import declared_attr
from sqlalchemy.ext.declarative import has_inherited_table

from gottlieb.model.auth import User
from gottlieb.lib import auth

from datetime import datetime as dt

class Base(object):
    """Базовая модель. Добавляет во всех наследников поле id и атрибут
    __tablename__ который заполняется автоматически. Имя таблицы берется из
    названия класса и переводится в нижний регистр. Таблица наследник имеет по
    умолчанию название и поле id, сильно облегчая жизнь.
    """

    @declared_attr
    def __tablename__(cls):
        if (has_inherited_table(cls) and
            Tablename not in cls.__bases__):
            return None
        return cls.__name__.lower()

    # Method "byId" for use in code like this:
    #   session.query(Table).byId(5)
    #
    # SQL statement like:
    #   SELECT * FROM Table WHERE id = 5;
    @classmethod
    def byId(cls, id) :
        return Session.query(cls).filter_by(id = id).first()

    id =  Column(Integer, autoincrement=True, primary_key=True)

class CreatedMixin(object):
    """Абстрактная примесь которая добавляет в другие модели поля:
        created_by - кто создал
        updated_by - последний кто обновил
        created_at - дата создания
        updated_at - дата последнего обновления
    Поля заполняются автоматически. Кто создал и обновил ссылаются на модель
    auth.User. Пользователь берется из текущей сессии, при помощи библиотеки 
    lib.auth и метода get_user
    """

    @declared_attr
    def created_by(cls):
        return Column(Integer, ForeignKey('user.user_id',
                      onupdate="cascade", ondelete="restrict"))

    @declared_attr
    def updated_by(cls):
        return Column(Integer, ForeignKey('user.user_id',
                      onupdate="cascade", ondelete="restrict"))

    created_at = Column(DateTime, nullable=False, default=dt.now())
    updated_at = Column(DateTime, nullable=False, default=dt.now(),
                        onupdate=dt.now())

Для полной картины приведу аналогичный пример на Django + DjangoORM.
Создадим файл myapp/accompaniment/models.py
from django.db import models
from django.contrib.auth.models import User

# Create your models here.
class ExtendedModel(models.Model):
    created_by = models.ForeignKey(User, null=True, blank=True, 
                 editable=False, related_name='%(class)s_creator')
    created_time = models.DateTimeField(auto_now_add=True, editable=False)
    modified_by = models.ForeignKey(User, null=True, blank=True, 
                 editable=False, related_name='%(class)s_modifier')
    modified_time = models.DateTimeField(auto_now=True, editable=False)

    class Meta:
        abstract = True
В папке accompaniment я привык держать всякие такие хелперы для проекта. Теперь используем эту модель в нашем проекте myapp/projectname/models.py
from django.db import models
from accompaniment.models import ExtendedModel

class Ticket(ExtendedModel):

    OPEN_STATUS = 1
    REOPENED_STATUS = 2
    RESOLVED_STATUS = 3
    CLOSED_STATUS = 4
    DUPLICATE_STATUS = 5

    STATUS_CHOICES = (
        (OPEN_STATUS, _('Open')),
        (REOPENED_STATUS, _('Reopened')),
        (RESOLVED_STATUS, _('Resolved')),
        (CLOSED_STATUS, _('Closed')),
        (DUPLICATE_STATUS, _('Duplicate')),        
    )

    PRIORITY_CHOICES = (
        (1, _('1. Critical')),
        (2, _('2. High')),
        (3, _('3. Normal')),
        (4, _('4. Low')),
        (5, _('5. Very Low')),
    )

    title = models.CharField(
        _('Title'),
        max_length=200,
        )

    queue = models.ForeignKey(
        Queue,
        verbose_name=_('Queue'),
        )

    assigned_to = models.ForeignKey(
        User,
        related_name='assigned_to',
        blank=True,
        null=True,
        verbose_name=_('Assigned to'),
        )

    status = models.IntegerField(
        _('Status'),
        choices=STATUS_CHOICES,
        default=OPEN_STATUS,
        )

    description = models.TextField(
        _('Description'),
        blank=True,
        null=True,
        help_text=_('The content of the customers query.'),
        )

    priority = models.IntegerField(
        _('Priority'),
        choices=PRIORITY_CHOICES,
        default=3,
        blank=3,
        )

    class Meta:
        get_latest_by = "created"
        verbose_name = u'Заявки'
        verbose_name_plural = u'Заявки'
   
    def __unicode__(self):
        return u'%s' % self.title

    def save(self, force_insert=False, force_update=False):
        if not self.priority:
            self.priority = 3

        super(Ticket, self).save(force_insert, force_update)

Такой несложный метод освобождает нас от размножения кучи одинаковых полей в моделях.

Update: в моделях Mixin был атрибут __abstract__ = True это неправильно, так-как все таблицы стают абстрактными. НО! Это вполне прокатит на версиях меньше 0.7, там этот атрибут почему-то не учитывается. На 0.7 версии работает как надо. Вот описание проблемы: stackoverflow

24 октября 2011

SQLalchemy UML диаграмма

Для визуализации своей базы в SQLAlchemy удобно использовать graphviz и библиотеку sqlalchemy_schemadisplay.
Установка:
apt-get install graphviz
pip install sqlalchemy_schemadisplay
Далее читаем доки SQLAlchemy Schema Display
Для Ъ:
Схема БД строится на основании данных базы.

from sqlalchemy import MetaData
from sqlalchemy_schemadisplay import create_schema_graph

# create the pydot graph object by autoloading all tables via a bound metadata object
graph = create_schema_graph(metadata=MetaData('postgres://user:pwd@host/database'),
   show_datatypes=False, # The image would get nasty big if we'd show the datatypes
   show_indexes=False, # ditto for indexes
   rankdir='LR', # From left to right (instead of top to bottom)
   concentrate=False # Don't try to join the relation lines together
)
graph.write_png('dbschema.png') # write out the file

Схема БД Postgres

Схема UML строится по моделям проекта.

from myapp import model
from sqlalchemy_schemadisplay import create_uml_graph
from sqlalchemy.orm import class_mapper

# lets find all the mappers in our model
mappers = []
for attr in dir(model):
    if attr[0] == '_': continue
    try:
        cls = getattr(model, attr)
        mappers.append(class_mapper(cls))
    except:
        pass

# pass them to the function and set some formatting options
graph = create_uml_graph(mappers,
    show_operations=False, # not necessary in this case
    show_multiplicity_one=False # some people like to see the ones, some don't
)
graph.write_png('schema.png') # write out the file

Схема моделей в Pylons
 
Для Django кодеров есть модуль django-extension который добавляет много полезных команд для manage.py. Вот мой вариант скрипта для Django:
project_dir/_visualozation/visualized.sh
curent_d="`date +%H%M_%d%m%y`" 
exec python ../manage.py graph_models -a -g -o scheme_of_$curent_d.png 

пример django-extension + graphviz
UPD: sadisplay - замечательная штука!