Commits

Vladimir Mihailenco committed e900ab9

Initial commit

  • Participants

Comments (0)

Files changed (11)

+syntax: glob
+*.pyc
+============
+Installation
+============
+
+::
+
+    pip install djsqlalchemy
+
+=====
+Usage
+=====
+
+Your models.py can look like this::
+
+    from django.db import models
+
+    from djsqlalchemy.api import DjSqlAlchemyManager
+    from djsqlalchemy.api import ManagerPlaceholder
+
+
+    class ComplexQueryManager(DjSqlAlchemyManager):
+        def get_select(self):
+            item = self.tables['djsqlalchemy_item']
+            category2 = self.tables['djsqlalchemy_category'].alias('category2')
+            return (item.select(use_labels=True).column(category2)
+                        .select_from(item.join(category2,
+                                               category2.c.id == item.c.category2_id)))
+
+
+    class Category(models.Model):
+        name = models.CharField(max_length=32)
+
+        def __unicode__(self):
+            return '%s - %s' % (self.pk, self.name)
+
+
+    class Item(models.Model):
+        name = models.CharField(max_length=32)
+        category1 = models.ForeignKey(Category, null=True, related_name='+')
+        category2 = models.ForeignKey(Category, null=True, related_name='+')
+        category3 = models.OneToOneField(Category, null=True, related_name='+')
+
+        objects = models.Manager()
+        complex_query = ManagerPlaceholder(ComplexQueryManager)
+
+        def __unicode__(self):
+            return '%s - %s' % (self.pk, self.name)
+
+In your views.py you can use new manager `complex_query` (it implements Django-like API, but it's not real manager), which uses SqlAlchemy Core to build complex queries::
+
+    sample_category = Category.objects.create(name='sample category')
+    sample_item = Item.objects.create(name='sample item',
+                                      category2=sample_category)
+
+    item = Item.complex_query.get()
+    assert item == sample_item
+    assert item._category2_cache == sample_category
+
+======================
+Implementation details
+======================
+
+- SqlAlchemy uses Django connection. Implemented using custom pool: `djsqlalchemy.alchemy.DjangoPool`.
+- Mapper uses fields names in it's work. You have to use SqlAlchemy `alias()` and `use_labels=True`.

File djsqlalchemy/__init__.py

+

File djsqlalchemy/alchemy.py

+from django.dispatch import receiver
+from django.db.backends.signals import connection_created
+
+from sqlalchemy import MetaData
+from sqlalchemy import create_engine
+from sqlalchemy.pool import NullPool
+from sqlalchemy.pool import _ConnectionRecord as _ConnectionRecordBase
+
+
+__all__ = ['get_engine', 'get_meta', 'get_tables']
+
+
+class mem(object):
+    """Module level cache"""
+    pass
+
+
+SQLALCHEMY_ENGINES = {
+    'sqlite3': 'sqlite',
+    'mysql': 'mysql',
+    'postgresql': 'postgresql',
+    'postgresql_psycopg2': 'postgresql+psycopg2',
+    'oracle': 'oracle',
+}
+
+
+def get_connection_string():
+    from django.db import connection
+
+    sett = connection.settings_dict
+    engine = sett['ENGINE']
+    engine = engine.replace('django.db.backends.', '')
+    engine = SQLALCHEMY_ENGINES[engine]
+    port = ':' + sett['PORT'] if sett['PORT'] else ''
+
+    str = '{engine}://{user}:{password}@{host}{port}/{name}'.format(
+        engine=engine, name=sett['NAME'], user=sett['USER'],
+        password=sett['PASSWORD'], host=sett['HOST'], port=port)
+
+    return str
+
+
+def get_engine():
+    if not getattr(mem, 'engine', None):
+        mem.engine = create_engine(get_connection_string(),
+                                   pool=DjangoPool(creator=None))
+    return mem.engine
+
+
+def get_meta():
+    if not getattr(mem, 'meta', None):
+        engine = get_engine()
+
+        mem.meta = MetaData()
+        mem.meta.reflect(bind=engine)
+    return mem.meta
+
+
+def get_tables():
+    return get_meta().tables
+
+
+class DjangoPool(NullPool):
+    def status(self):
+        return "DjangoPool"
+
+    def _create_connection(self):
+        return _ConnectionRecord(self)
+
+    def recreate(self):
+        self.logger.info("Pool recreating")
+
+        return DjangoPool(self._creator,
+            recycle=self._recycle,
+            echo=self.echo,
+            logging_name=self._orig_logging_name,
+            use_threadlocal=self._use_threadlocal,
+            _dispatch=self.dispatch)
+
+
+class _ConnectionRecord(_ConnectionRecordBase):
+    def __init__(self, pool):
+        self.__pool = pool
+        self.info = {}
+
+        pool.dispatch.first_connect.exec_once(self.connection, self)
+        pool.dispatch.connect(self.connection, self)
+
+    @property
+    def connection(self):
+        from django.db import connection
+        if connection.connection is None:
+            connection._cursor()
+        return connection.connection
+
+    def close(self):
+        pass
+
+    def invalidate(self, e=None):
+        pass
+
+    def get_connection(self):
+        return self.connection

File djsqlalchemy/api.py

+from djsqlalchemy.alchemy import *
+from djsqlalchemy.mapper import *
+from djsqlalchemy.managers import *

File djsqlalchemy/managers.py

+from django.db import models
+
+from djsqlalchemy.alchemy import get_tables
+from djsqlalchemy.mapper import get_list
+from djsqlalchemy.mapper import get_one
+from djsqlalchemy.utils import cached_property
+
+
+class DjSqlAlchemyManager(object):
+    def __init__(self, model, *args, **kwargs):
+        self.model = model
+
+    @cached_property
+    def tables(self):
+        return get_tables()
+
+    def get_select(self):
+        raise NotImplementedError()
+
+    def all(self):
+        if hasattr(self, 'result'):
+            return self.result
+        else:
+            return get_list(self.get_select(), self.model)
+
+    def get(self):
+        if hasattr(self, 'result'):
+            return self.result.pop()
+        else:
+            return get_one(self.get_select(), self.model)
+
+    def count(self):
+        if hasattr(self, 'result'):
+            return len(self.result)
+        else:
+            s = self.get_select().alias('row').count()
+            return get_one(s)[0]
+
+    def get_query_set(self):
+        raise NotImplementedError('Not supported')
+
+
+class ManagerPlaceholder(models.Manager):
+    def __init__(self, manager):
+        super(ManagerPlaceholder, self).__init__()
+        self.manager = manager
+
+    def contribute_to_class(self, model, name):
+        self.model = model
+        setattr(model, name, self)
+        if (not getattr(model, '_default_manager', None) or
+            self.creation_counter < model._default_manager.creation_counter):
+            model._default_manager = self
+        if (model._meta.abstract or
+                (self._inherited and not self.model._meta.proxy)):
+            model._meta.abstract_managers.append((self.creation_counter, name,
+                    self))
+        else:
+            model._meta.concrete_managers.append((self.creation_counter, name,
+                self))
+
+    def __get__(self, instance, owner):
+        if instance != None:
+            raise AttributeError("Manager isn't accessible via %s instances" %
+                                 type.__name__)
+        return self.manager(self.model)

File djsqlalchemy/mapper.py

+from django.db.models import ForeignKey
+
+from djsqlalchemy.alchemy import get_engine
+
+
+__all__ = ['fetchall', 'get_list', 'get_one']
+
+
+def map_related(obj, values, prefix='', counter=None):
+    # special case for master obj
+    local_prefix = prefix or (obj._meta.db_table + '_')
+
+    fks = []
+    for field in obj._meta.local_fields:
+        if isinstance(field, ForeignKey):
+            fks.append(field)
+        else:
+            label = local_prefix + field.name
+            if label in values:
+                setattr(obj, field.name, values[label])
+                del values[label]
+
+    if obj.pk:
+        for fk in fks:
+            rel_obj = fk.rel.to()
+            map_related(rel_obj, values, prefix=prefix + fk.name + '_')
+            if rel_obj.pk:
+                cache_name = '_%s_cache' % fk.name
+                setattr(obj, cache_name, rel_obj)
+
+
+def map_values(model, values):
+    obj = model()
+    values = dict(values)
+    map_related(obj, values)
+
+    # set other values on master obj as is
+    for label, value in values.items():
+        setattr(obj, label, value)
+    return obj
+
+
+def fetchall(s, model=None):
+    engine = get_engine()
+
+    result = engine.execute(s)
+    values_list = result.fetchall()
+
+    for values in values_list:
+        if model:
+            yield map_values(model, values)
+        else:
+            yield values
+
+
+def get_list(s, model=None):
+    return list(fetchall(s, model))
+
+
+def get_one(s, model=None):
+    objs = list(fetchall(s, model))
+
+    if not objs:
+        if model:
+            raise model.DoesNotExist()
+        else:
+            raise ValueError('Query returned empty result')
+    elif len(objs) > 1:
+        if model:
+            raise model.MultipleObjectsReturned()
+        else:
+            raise ValueError('Query returned multiple rows')
+
+    return objs[0]

File djsqlalchemy/models.py

+from django.db import models
+
+from djsqlalchemy.api import DjSqlAlchemyManager
+from djsqlalchemy.api import ManagerPlaceholder
+
+
+class ComplexQueryManager(DjSqlAlchemyManager):
+    def get_select(self):
+        item = self.tables['djsqlalchemy_item']
+        category2 = self.tables['djsqlalchemy_category'].alias('category2')
+        return (item.select(use_labels=True).column(category2)
+                    .select_from(item.join(category2,
+                                           category2.c.id == item.c.category2_id)))
+
+
+class Category(models.Model):
+    name = models.CharField(max_length=32)
+
+    def __unicode__(self):
+        return '%s - %s' % (self.pk, self.name)
+
+
+class Item(models.Model):
+    name = models.CharField(max_length=32)
+    category1 = models.ForeignKey(Category, null=True, related_name='+')
+    category2 = models.ForeignKey(Category, null=True, related_name='+')
+    category3 = models.OneToOneField(Category, null=True, related_name='+')
+
+    objects = models.Manager()
+    complex_query = ManagerPlaceholder(ComplexQueryManager)
+
+    def __unicode__(self):
+        return '%s - %s' % (self.pk, self.name)

File djsqlalchemy/tests/__init__.py

+from django import test
+from django.db import transaction
+
+from djsqlalchemy.models import Category
+from djsqlalchemy.models import Item
+
+from sqlalchemy.sql import select
+from djsqlalchemy.api import get_engine
+from djsqlalchemy.api import get_tables
+from djsqlalchemy.api import get_one
+
+
+class MapperTest(test.TestCase):
+    def test_simple_queries(self):
+        engine = get_engine()
+
+        tables = get_tables()
+        item_table = tables['djsqlalchemy_item']
+        category_table = tables['djsqlalchemy_category']
+
+        category = Category.objects.create(name='category')
+        item = Item.objects.create(name='item1', category1=category,
+            category2=category, category3=category)
+        transaction.commit()
+
+        s = item_table.select()
+        self.assertTrue(bool(engine.execute(s).fetchall()))
+
+        s = category_table.select()
+        self.assertTrue(bool(engine.execute(s).fetchall()))
+
+    def test_mapper(self):
+        tables = get_tables()
+        item_table = tables['djsqlalchemy_item']
+        category_table = tables['djsqlalchemy_category']
+
+        category1 = Category.objects.create(name='category1')
+        category2 = Category.objects.create(name='category2')
+        category3 = Category.objects.create(name='category3')
+
+        item = Item.objects.create(name='item1', category1=category1,
+            category2=category2, category3=category3)
+
+        transaction.commit()
+
+        category1_table = category_table.alias('category1')
+        category2_table = category_table.alias('category2')
+        category3_table = category_table.alias('category3')
+        s = select([item_table, category1_table, category2_table, category3_table],
+            from_obj=[
+                item_table.join(category1_table,
+                                category1_table.c.id == item_table.c.category1_id)
+                          .join(category2_table,
+                                category2_table.c.id == item_table.c.category2_id)
+                          .join(category3_table,
+                                category3_table.c.id == item_table.c.category3_id)
+            ], use_labels=True)
+
+        item_obj = get_one(s, Item)
+        self.assertEqual(item_obj, item)
+        self.assertEqual(item_obj._category1_cache, category1)
+        self.assertEqual(item_obj._category2_cache, category2)
+        self.assertEqual(item_obj._category3_cache, category3)
+
+        s = select([category_table], use_labels=True)
+        self.assertRaises(Category.MultipleObjectsReturned, get_one,
+                          s, Category)
+
+        s = select([category_table], category_table.c.id == 999,
+                   use_labels=True)
+        self.assertRaises(Category.DoesNotExist, get_one,
+                          s, Category)
+
+    def test_manager(self):
+        sample_category = Category.objects.create(name='sample category')
+        sample_item = Item.objects.create(name='sample item',
+                                          category2=sample_category)
+
+        item = Item.complex_query.get()
+        self.assertEqual(item, sample_item)
+        self.assertEqual(item._category2_cache, sample_category)

File djsqlalchemy/utils.py

+class _Missing(object):
+
+    def __repr__(self):
+        return 'no value'
+
+    def __reduce__(self):
+        return '_missing'
+
+_missing = _Missing()
+
+
+class cached_property(object):
+    """A decorator that converts a function into a lazy property. The
+function wrapped is called the first time to retrieve the result
+and then that calculated result is used the next time you access
+the value::
+
+class Foo(object):
+
+@cached_property
+def foo(self):
+# calculate something important here
+return 42
+
+The class has to have a `__dict__` in order for this property to
+work.
+
+.. versionchanged:: 0.6
+the `writeable` attribute and parameter was deprecated. If a
+cached property is writeable or not has to be documented now.
+For performance reasons the implementation does not honor the
+writeable setting and will always make the property writeable.
+"""
+
+    # implementation detail: this property is implemented as non-data
+    # descriptor. non-data descriptors are only invoked if there is
+    # no entry with the same name in the instance's __dict__.
+    # this allows us to completely get rid of the access function call
+    # overhead. If one choses to invoke __get__ by hand the property
+    # will still work as expected because the lookup logic is replicated
+    # in __get__ for manual invocation.
+
+    def __init__(self, func, name=None, doc=None, writeable=False):
+        if writeable:
+            from warnings import warn
+            warn(DeprecationWarning('the writeable argument to the '
+                                    'cached property is a noop since 0.6 '
+                                    'because the property is writeable '
+                                    'by default for performance reasons'))
+
+        self.__name__ = name or func.__name__
+        self.__module__ = func.__module__
+        self.__doc__ = doc or func.__doc__
+        self.func = func
+
+    def __get__(self, obj, type=None):
+        if obj is None:
+            return self
+        value = obj.__dict__.get(self.__name__, _missing)
+        if value is _missing:
+            value = self.func(obj)
+            obj.__dict__[self.__name__] = value
+        return value
+from distutils.core import setup
+
+
+setup(
+    name='djsqlalchemy',
+    version='0.1.0',
+    description='Django and SqlAlchemy integration',
+    author='Vladimir Mihailenco',
+    author_email='vladimir.webdev@gmail.com',
+    url='',
+    packages=['djsqlachemy', 'djsqlalchemy.tests'],
+    classifiers=[
+        'Development Status :: 2 - Pre-Alpha',
+        'Environment :: Web Environment',
+        'Framework :: Django',
+        'Intended Audience :: Developers',
+        'License :: OSI Approved',
+        'Operating System :: OS Independent',
+        'Programming Language :: Python',
+        'Topic :: Software Development :: Libraries :: Python Modules',
+    ],
+)