Skip to content

Commit 15c5157

Browse files
committed
Initial version. Incorporated tests from zzzeek's version
0 parents  commit 15c5157

File tree

4 files changed

+227
-0
lines changed

4 files changed

+227
-0
lines changed

LICENSE

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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.

README

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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.

sqlalchemy_django_query.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

tests.py

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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 commit comments

Comments
 (0)