Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Initial version. Incorporated tests from zzzeek's version

  • Loading branch information...
commit 15c5157cb3448e93ac64f01a256ba49647dc21ef 0 parents
Armin Ronacher authored

Showing 4 changed files with 227 additions and 0 deletions. Show diff stats Hide diff stats

  1. +31 0 LICENSE
  2. +6 0 README
  3. +109 0 sqlalchemy_django_query.py
  4. +81 0 tests.py
31 LICENSE
... ... @@ -0,0 +1,31 @@
  1 +Copyright (c) 2011 by Armin Ronacher and Mike Bayer.
  2 +
  3 +Some rights reserved.
  4 +
  5 +Redistribution and use in source and binary forms, with or without
  6 +modification, are permitted provided that the following conditions are
  7 +met:
  8 +
  9 + * Redistributions of source code must retain the above copyright
  10 + notice, this list of conditions and the following disclaimer.
  11 +
  12 + * Redistributions in binary form must reproduce the above
  13 + copyright notice, this list of conditions and the following
  14 + disclaimer in the documentation and/or other materials provided
  15 + with the distribution.
  16 +
  17 + * The names of the contributors may not be used to endorse or
  18 + promote products derived from this software without specific
  19 + prior written permission.
  20 +
  21 +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
  22 +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
  23 +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
  24 +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
  25 +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
  26 +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
  27 +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
  28 +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
  29 +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
  30 +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
  31 +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
6 README
... ... @@ -0,0 +1,6 @@
  1 +django_sqlalchemy_query
  2 +```````````````````````
  3 +
  4 +A module that implements Django like query objects for SQLAlchemy.
  5 +This repository is waiting for a pull request that adds a setup.py
  6 +and documentation. Look at the tests for some examples.
109 sqlalchemy_django_query.py
... ... @@ -0,0 +1,109 @@
  1 +# -*- coding: utf-8 -*-
  2 +"""
  3 + sqlalchemy_django_query
  4 + ~~~~~~~~~~~~~~~~~~~~~~~
  5 +
  6 + A module that implements a more Django like interface for SQLAlchemy
  7 + query objects. It's still API compatible with the regular one but
  8 + extends it with Djangoisms.
  9 +
  10 + :copyright: 2011 by Armin Ronacher, Mike Bayer.
  11 + license: BSD, see LICENSE for more details.
  12 +"""
  13 +from sqlalchemy.orm.query import Query
  14 +from sqlalchemy.orm.util import _entity_descriptor
  15 +from sqlalchemy.util import to_list
  16 +from sqlalchemy.sql import operators, extract
  17 +
  18 +
  19 +class DjangoQuery(Query):
  20 + """A subclass of a regular SQLAlchemy query object that implements
  21 + more Django like behavior:
  22 +
  23 + - `filter_by` supports implicit joining and subitem accessing with
  24 + double underscores.
  25 + - `exclude_by` works like `filter_by` just that every expression is
  26 + automatically negated.
  27 + - `order_by` supports ordering by field name with an optional `-`
  28 + in front.
  29 + """
  30 + _underscore_operators = {
  31 + 'gt': operators.gt,
  32 + 'lte': operators.lt,
  33 + 'gte': operators.ge,
  34 + 'le': operators.le,
  35 + 'contains': operators.contains_op,
  36 + 'in': operators.in_op,
  37 + 'exact': operators.eq,
  38 + 'iexact': operators.ilike_op,
  39 + 'startswith': operators.startswith_op,
  40 + 'istartswith': lambda c, x: c.ilike(x.replace('%', '%%') + '%'),
  41 + 'iendswith': lambda c, x: c.ilike('%' + x.replace('%', '%%')),
  42 + 'endswith': operators.endswith_op,
  43 + 'isnull': lambda c, x: x and c != None or c == None,
  44 + 'range': operators.between_op,
  45 + 'year': lambda c, x: extract('year', c) == x,
  46 + 'month': lambda c, x: extract('month', c) == x,
  47 + 'day': lambda c, x: extract('day', c) == x
  48 + }
  49 +
  50 + def filter_by(self, **kwargs):
  51 + return self._filter_or_exclude(False, kwargs)
  52 +
  53 + def exclude_by(self, **kwargs):
  54 + return self._filter_or_exclude(True, kwargs)
  55 +
  56 + def order_by(self, *args):
  57 + args = list(args)
  58 + joins_needed = []
  59 + for idx, arg in enumerate(args):
  60 + q = self
  61 + if not isinstance(arg, basestring):
  62 + continue
  63 + if arg[0] in '+-':
  64 + desc = arg[0] == '-'
  65 + arg = arg[1:]
  66 + else:
  67 + desc = False
  68 + q = self
  69 + column = None
  70 + for token in arg.split('__'):
  71 + column = _entity_descriptor(q._joinpoint_zero(), token)
  72 + if column.impl.uses_objects:
  73 + q = q.join(column)
  74 + joins_needed.append(column)
  75 + column = None
  76 + if column is None:
  77 + raise ValueError('Tried to order by table, column expected')
  78 + if desc:
  79 + column = column.desc()
  80 + args[idx] = column
  81 +
  82 + q = Query.order_by(self, *args)
  83 + for join in joins_needed:
  84 + q = q.join(join)
  85 + return q
  86 +
  87 + def _filter_or_exclude(self, negate, kwargs):
  88 + q = self
  89 + negate_if = lambda expr: expr if not negate else ~expr
  90 + column = None
  91 +
  92 + for arg, value in kwargs.iteritems():
  93 + for token in arg.split('__'):
  94 + if column is None:
  95 + column = _entity_descriptor(q._joinpoint_zero(), token)
  96 + if column.impl.uses_objects:
  97 + q = q.join(column)
  98 + column = None
  99 + elif token in self._underscore_operators:
  100 + op = self._underscore_operators[token]
  101 + q = q.filter(negate_if(op(column, *to_list(value))))
  102 + column = None
  103 + else:
  104 + raise ValueError('No idea what to do with %r' % token)
  105 + if column is not None:
  106 + q = q.filter(negate_if(column == value))
  107 + column = None
  108 + q = q.reset_joinpoint()
  109 + return q
81 tests.py
... ... @@ -0,0 +1,81 @@
  1 +import unittest
  2 +from sqlalchemy import Column, Integer, String, ForeignKey, Date, create_engine
  3 +from sqlalchemy.orm import Session, relationship
  4 +from sqlalchemy.ext.declarative import declarative_base, declared_attr
  5 +import datetime
  6 +
  7 +from sqlalchemy_django_query import DjangoQuery
  8 +
  9 +
  10 +class BasicTestCase(unittest.TestCase):
  11 +
  12 + def setUp(self):
  13 + class Base(object):
  14 + @declared_attr
  15 + def __tablename__(cls):
  16 + return cls.__name__.lower()
  17 + id = Column(Integer, primary_key=True)
  18 + Base = declarative_base(cls=Base)
  19 +
  20 + class Blog(Base):
  21 + name = Column(String)
  22 + entries = relationship('Entry', backref='blog')
  23 +
  24 + class Entry(Base):
  25 + blog_id = Column(Integer, ForeignKey('blog.id'))
  26 + pub_date = Column(Date)
  27 + headline = Column(String)
  28 + body = Column(String)
  29 +
  30 + engine = create_engine('sqlite://')
  31 + Base.metadata.create_all(engine)
  32 + self.session = Session(engine, query_cls=DjangoQuery)
  33 + self.Base = Base
  34 + self.Blog = Blog
  35 + self.Entry = Entry
  36 + self.engine = engine
  37 +
  38 + self.b1 = Blog(name='blog1', entries=[
  39 + Entry(headline='b1 headline 1', body='body 1',
  40 + pub_date=datetime.date(2010, 2, 5)),
  41 + Entry(headline='b1 headline 2', body='body 2',
  42 + pub_date=datetime.date(2010, 4, 8)),
  43 + Entry(headline='b1 headline 3', body='body 3',
  44 + pub_date=datetime.date(2010, 9, 14))
  45 + ])
  46 + self.b2 = Blog(name='blog2', entries=[
  47 + Entry(headline='b2 headline 1', body='body 1',
  48 + pub_date=datetime.date(2010, 5, 12)),
  49 + Entry(headline='b2 headline 2', body='body 2',
  50 + pub_date=datetime.date(2010, 7, 18)),
  51 + Entry(headline='b2 headline 3', body='body 3',
  52 + pub_date=datetime.date(2011, 8, 27))
  53 + ])
  54 +
  55 + self.session.add_all([self.b1, self.b2])
  56 + self.session.commit()
  57 +
  58 + def test_basic_filtering(self):
  59 + bq = self.session.query(self.Blog)
  60 + eq = self.session.query(self.Entry)
  61 + assert bq.filter_by(name__exact='blog1').one() is self.b1
  62 + assert bq.filter_by(name__contains='blog').all() == [self.b1, self.b2]
  63 + assert bq.filter_by(entries__headline__exact='b2 headline 2').one() is self.b2
  64 + assert bq.filter_by(entries__pub_date__range=(datetime.date(2010, 1, 1),
  65 + datetime.date(2010, 3, 1))).one() is self.b1
  66 + assert eq.filter_by(pub_date__year=2011).one() is self.b2.entries[2]
  67 + assert eq.filter_by(pub_date__year=2011, id=self.b2.entries[2].id
  68 + ).one() is self.b2.entries[2]
  69 +
  70 + def test_basic_excluding(self):
  71 + eq = self.session.query(self.Entry)
  72 + assert eq.exclude_by(pub_date__year=2010).one() is self.b2.entries[2]
  73 +
  74 + def test_basic_ordering(self):
  75 + eq = self.session.query(self.Entry)
  76 + assert eq.order_by('-blog__name', 'id').all() == \
  77 + self.b2.entries + self.b1.entries
  78 +
  79 +
  80 +if __name__ == '__main__':
  81 + unittest.main()

0 comments on commit 15c5157

Please sign in to comment.
Something went wrong with that request. Please try again.