dhp.doq package

Module contents

Data Object Query mapper.

pronounced Duke allows you to query an list, iterable or generator yielding objects with a Django ORM like / Fluent interface. This is useful for exploratory programming and also it is just a nice, comfortable inteface to query your data objects.

Example

Say you had a csv file of employee records and you wanted to list the employees in the IT department. Well you could do the traditional thing or ...

Example:

# bread and butter Python
EmployeeRecord = namedtuple('EmployeeRecord', 'emp_id, name, dept, hired')

def csvtuples():
    '''csv named tuple generator.'''
    reader = csv.reader(TEST_FILE)
    for emp in map(EmployeeRecord._make, reader):
        yield emp

# Enter the Duke
doq = DOQ(data_objects=csvtuples())
for emp in doq.filter(dept='IT'):
    print(emp)

# Now let's list everyone who is not in IT.
for emp in doq.exclude(dept='IT'):
    print(emp)

# ok, now let's sort the not IT employees by name
for emp in doq.exclude(dept='IT').order_by('name'):
    print(emp)

Yes, it is just that easy. You can chain filter() and exclude(). There is a get() method that raises DoesNotExist() and MultipleObjectsReturned().

All that ooohey gooey query goodness of a traditional ORM but quick and easy and works without a lot of setup.

One quick note before we head into the full documenation. DOQ is NOT a full blown Object Relation Manager. It does not create databases, nor know how to access them. If that is what you desire, then SQLAlchemy, Pony, PeeWeeDB or Django’s ORM is probably going to get you what you want.

If you are looking to slap some lipstick on a simple data source, well then, DOQ is just your color.

class dhp.doq.DOQ(data_objects)

Bases: object

data object query mapper.

all()

Returns a cloned DOQ. Short hand for an empty filter but it reads more naturally than doq.filter().

Parameters:None
Returns:A cloned DOQ object.
Return type:DOQ

Example:

for obj in doq.all():
    print(obj)
count

A property that returns the number of objects currently selected. Can also use len(doq).

Returns:The number of objects selected.
Return type:(int)

Example:

if doq.filter(name='Jeff').count == 1:
    do_something
result = doq.filter(emp_id=1)
assert doq.count == len(doq)
exclude(**look_ups)

Returns a new DOQ containing objects that do not match the given lookup parameters.

Parameters:look_ups – The lookup parameters should be in the format described in Attribute Lookups below. Multiple parameters are joined via AND in the underlying logic, and the whole thing is enclosed in a NOT.
Returns:A cloned DOQ object with the specified exclude(s).
Return type:DOQ
Raises:AttributeError – If an attribute_name in the look_ups specified can not be found.

This example excludes all entries whose hired date is later than 2005-1-3 AND whose name is “Jeff”:

doq.exclude(hired__gt=datetime.date(2005, 1, 3), name='Jeff')
filter(**look_ups)

Returns a new DOQ containing objects that match the given lookup parameters.

Parameters:look_ups – The lookup parameters should be in the format described in Attribute Lookups below. Multiple parameters are joined via AND in the underlying logic.
Returns:A cloned DOQ object with the specified filter(s).
Return type:DOQ
Raises:AttributeError – If an attribute_name in the look_ups specified can not be found.

Example:

doq.filter(name='Foo', hired__gte='2012-01-03')
get(**look_ups)

Preform a get operation using 0 or more filter keyword arguments. A single object should be returned.

Parameters:look_ups – The lookup parameters should be in the format described in Attribute Lookups below. Multiple parameters are joined via AND in the underlying logic.
Returns:A single matching data_object from data_objects.
Return type:data_object
Raises:AttributeError – If an attribute_name in the look_ups specified can not be found.

Example:

obj = doq.get(emp_id=1)
Raises:
static get_attr(obj, attrname)

Retrieve a possibly nested attribute value.

Parameters:
  • obj (data object) – The data object to retrieve the value.
  • attrname (str) – The attribute name/path to retrieve. A simple object access might be name, a nested object value might be address__city
Returns:

The value of the indicated attribute.

order_by(*attribute_names)

Return a new DOQ with thes results ordered by the data_object’s attribute(s). The default order is assending. Use a minus (-) sign in front of the attribute name to indicate descending order. Repeated .order_by calls are NOT additive, they replace any existing ordering.

Parameters:attribute_names – 0 or more data_object attribute names. Listed from most significant order to least.
Returns:A new DOQ object with the specified ordering.
Return type:DOQ

Example:

doq.all().order_by('emp_id')  # emp_id 1, 2, 3, ..., n
doq.all().order_by('-emp_id') # emp_id n, n-1, n-2, ..., 1

doq.all().order_by('dept', 'emp_id') # by dept, then by emp_id

to order randomly, use a ‘?’.

doq.all().order_by('?')
static order_by_key_fn(attrname)

Override this method to supply a new key function for the order_by method.

The default function is:

lambda obj: DOQ.get_attr(obj, attrname)

If you had an attribute “emp_id” that returned a number as a string ['2', '1', '3', '11']. It would be ordered by string conventions returning them in ['1', '11', '2', '3']. If you want them sorted like integers ['1', '2', '3', '11'], you would subclass DOQ and override the `order_by_key_fn like this:

class MyDOQ(DOQ):
    @staticmethod
    def order_by_key_fn(attrname):
        if attrname == 'emp_id':
            def key_fn(obj):
                # return attr as an integer
                return int(DOQ.get_attr(obj, attrname))
        else:
            def key_fn(obj):
                # return the standard function.
                return DOQ.get_attr(obj, attrname)
        return key_fn

mydoq = MyDOQ(data_objects)
mydoq.all().order_by('emp_id')
Parameters:attrname (str) – The attribute name be acted on by the order_by method.
Returns:
A function that takes the attribute name as an argument
and that also has access to the object be acted on.
Return type:function
Raises:AttributeError – If the attribute_name specified can not be found.
ordered

True if an order is set, otherwise False.

Returns:True if the order_by is set, otherwise False.
Return type:bool

Example:

results = doq.all()
assert results.ordered == False
results = results.order_by('name')
assert results.ordered == True
exception dhp.doq.DoesNotExist

Bases: exceptions.Exception

Raised when no object is found.

exception dhp.doq.MultipleObjectsReturned

Bases: exceptions.Exception

raised when more than 1 object returned but should not be.

Attribute Lookups

Attribute lookups are similar to how you specify the meat of an SQL WHERE clause. They’re specified as keyword arguments to the DOQ methods filter(), exclude() and get().

The format of look_ups is attribute_name__operation=value That is the name of the attribute to look at, a double under score(dunder) and then the lookup operator, an equals sign and then the value to compare against. The format was inspired by Django’s ORM.

DOQ’s inbuilt lookups are listed below.

As a convenience when no lookup type is provided (like in doq.get(emp_id=14)) the lookup type is assumed to be exact.

exact

Exact case-sensitive match.

doq.get(emp_id__exact=4)
assert doq.get(name='Jeff') == doq.get(name__exact='Jeff')

iexact

Exact, case insensitive, match.

doq.filter(name__iexact='jeff')  # would match, jEFF, Jeff, etc.

lt

Less Than.

doq.filter(emp_id__lt=3)        # given [4, 3, 2, 1], would match [2, 1]

lte

Less Than or Equal to.

doq.filter(emp_id__lte=3)        # given [4, 3, 2, 1], would match [3, 2, 1]

gt

Greater Than.

doq.filter(emp_id__gt=3)        # given [4, 3, 2, 1], would match [4, ]

gte

Greater Than or Equal To.

doq.filter(emp_id__gte=3)        # given [4, 3, 2, 1], would match [4, 3]

contains

If the value is in the attribute.

doq.filter(name__contains='o') # given ['Oscar', 'John', 'Jo'], would match ['John', 'Joe']

icontains

Case insensitive version of contains. See above.

doq.filter(name__icontains='o') # given ['Oscar', 'John', 'Jo'], would match ['Oscar', John', 'Joe']

startswith

If the attribute value startswith.

doq.filter(name__startswith='O') # given ['Oscar', 'John', 'Jo'], would match ['Oscar', ]

istartswith

Case insensitive version of startswith. See above.

doq.filter(name__istartswith='o') # given ['Oscar', 'John', 'Jo'], would match ['Oscar', ]

endswith

If the attribute value endswith.

doq.filter(name__endswith='n') # given ['Oscar', 'John', 'Jo'], would match ['John', ]

iendswith

Case insensitive version of endswith. See above.

doq.filter(name__iendswith='N') # given ['Oscar', 'John', 'Jo'], would match ['John', ]

in

If the attribute value is in the list supplied.

doq.filter(emp_id__in=[1, 3])   # given [1, 2, 3, 4], would match [1, 3]

range

Is a short hand equivalent of a >= b and a <= c where a__range=(b, c) and b <= c

doq.filter(emp_id__range=(2, 5))  # is equivalent of doq.filter(emp_id__gte=2, emp_id__lte=5)

Nested Objects

If you have a object that is composed of nested objects, you can access the values of the nested subobjects by using double underscores to list the path of the relationship. Say you had a list of objects with the following layout:

user:
    id
    name
    address:
        street
        suite
        zipcode
        geo:
            lat
            lon

You would access the top-level attributes.

doq.filter(id=7)`

To access the suite information,

doq.filter(address__suite='Apt. 201')

which would be an exact match on the attribute value. To use another operator with your lookup just specify it.

doq.filter(address__suite__startswith='Apt.')

Ordering on a nested attribute is the same. To order by lat:

doq.all().order_by('address__geo__lat')

Slicing DOQ (Limiting)

Slicing a DOQ is supported. Since we are not performing SQL the results of a slicing operation are immediate and return a list of data_objects.

>>> type(doq.all()[2:4])
<type 'list'>

This also means that Negative indexing is supported.

doq.all()[-1]

Would return the last data_object from the results.