DivmodAxiom : Reference

Item

item.Item(store=store.Store, **kw)

A class describing an item in an Axiom Store. Your subclass of axiom.item.Item may define the following special attributes:

  • typeName: A string uniquely identifying this class schema in the store. If omitted, a normalised typeName will automatically be generated. If such an item class is refactored to a new module you will need to set typeName to the previous normalised value by passing the previous fully qualified class name, and passing it to axiom.item.normalize(). (See #399)
  • schemaVersion: An integer denoting the schema version. If omitted, this defaults to 1. In subsequent schema versions the schemaVersion must be explicitly defined. See DivmodAxiom/Reference#Upgraders. (See #398)

You will also want to define other attributes and these must be special Axiom class attributes. A selection of standard types are already defined in the axiom.attributes module. A simple example:

from axiom import item, attributes

class ShopProduct(item.Item):
    typeName = "ShopProduct"

    name = attributes.text(allowNone=False)
    price = attributes.integer(allowNone=False)
    stock = attributes.integer(default=0)
    
    def __repr__(self):
        return '<ShopProduct name="%s" price="%d" stock="%d">' % (self.name, self.price, self.stock)

Limitations

  • Axiom Items only support one level of inheritance. You could not for example write a subclass of the ShopProduct class above. This is by design, and you are encouraged to explore object composition and adaption instead.

Store

store.Store([dbdir=None[, debug=False[, parent=None[, idInParent=None]]]])

A database in which Axiom Items can be stored. An Axiom Store can be instantiated with a dbdir parameter, in which case it will be persisted to the filesystem at the given path. Alternatively, if instantiated without a dbdir parameter, the store will exist inmemory only for the lifetime of the python process.

from axiom import store

s = store.Store('/tmp/example.axiom')
s = store.Store() # An inmemory store 

If debug=True, the store will print out all sql commands as they are issued to the underlying Sqlite database.

Add Items to the Store

p = ShopProduct(store=s, name=u'Tea Bags', price=2)

That's all there is to it. The returned item can be treated just like any other python object. Changes made to it are automatically persisted to the store.

>>> p.name
u'Tea Bags'
>>> p.stock
0
>>> p.stock += 20
>>> p.stock
20

If you want to avoid duplicate items you can instead use the findOrCreate method (see below)

Retrieve Items from the Store

getItemByID(storeID[, default=_noItem])
Returns the item with the given "storeID". If no matching item raises KeyError or "default" if given. Every item in the store has a unique "storeID" attribute.
findFirst(tableClass [, query arguments except 'limit'])
Returns the first item of class "tableClass" matching the query "comparison" or None, eg.
>>> s.findFirst(ShopProduct, ShopProduct.name==u'Tea Bags')
<ShopProduct name="Tea Bags" price="2" stock="20">
findOrCreate(userItemClass[, **attrs])
Returns the first item of class "userItemClass" or creates it if it doesn't already exist. eg.
>>> s.findOrCreate(ShopProduct, name=u'Pot Noodle')
TypeError: attribute [ShopProduct.price = integer()] must not be None

but we must give all attributes required to create the new item

>>> s.findOrCreate(ShopProduct, name=u'Pot Noodle', price=3)
<ShopProduct name="Pot Noodle" price="3" stock="0">
query(tableClass[, comparison=None[, limit=None[, offset=None[, sort=None]]]])
Return generator of items matching class "tableClass" and "comparison". Limited to length "limit" beyond "offset". Sorted by attribute "sort". Examples:
>>> "All products"
>>> list(s.query(ShopProduct))
[<ShopProduct name="Tea Bags" price="2" stock="20">,
<ShopProduct name="Pot Noodle" price="3" stock="0">]

>>> "Products in stock"
>>> list(s.query(ShopProduct, ShopProduct.stock > 0))
[<ShopProduct name="Tea Bags" price="2" stock="20">]

>>> "Products in stock AND which cost less than 5"
>>> from axiom.attributes import AND, OR
>>> list(s.query(ShopProduct, AND(ShopProduct.stock > 0, ShopProduct.price < 5)))
[<ShopProduct name="Tea Bags" price="2" stock="20">]

If you only want a single attribute from a query, you can use a ColumnQuery.

>>> list(s.query(ShopProduct, sort=ShopProduct.stock.ascending).getColumn('name'))
[u'Pot Noodle', u'Tea Bags']

This also demonstrates how to sort the results.

Axiom is also capable of constructing more complex queries involving table joins behind the scenes. For more complete examples see [DivmodAxiom/Examples]

SubStore

substore.SubStore(store, path, *a, **kw)

XXX most of the stuff about SubStore instantiation is wrong now -- createNew?

A Store that also exists as an Item in a parent Store.

  • store: a reference to a parent store;
  • path: a string or an array representing the relative path to the substore from the files subdirectory of the parent store.

Example:

>>> from axiom import store, substore
>>> s = store.Store('/tmp/substoretest.axiom')
>>> ssItem = substore.SubStore(s, ['path', 'to', 'substore'])
>>> s
<Store '/tmp/substoretest.axiom'@0xb7d36f6c>
>>> ssItem
<axiom.substore.SubStore object at 0xb79a16a4>

The SubStore is just another Item. It's the store within that you will probably want.

>>> ss = ssItem.open() 
<Store '/tmp/substoretest.axiom/files/path/to/substore'@0xb780df8c>

Calling SubStore.open() is not recommended however. Instead register an adaptor for substore.SubStore. When you adapt the SubStore, your adaptor will actually be passed the Store. eg.

from zope.interface import Interface, implements
from twisted.python.components import registerAdapter

class IMyApp(Interface):
    pass

class MyApp(object):
    implements(IMyApp)
    def __init__(self, original): 
        self.original = original 

registerAdapter(MyApp, store.Store, IMyApp)
>>> IMyApp(ssItem).original
<Store '/tmp/substoretest.axiom/files/path/to/substore'@0xb77e158c>

Now given a simple Item...

from axiom import item, attributes

class MenuItem(item.Item):
    typeName = "MenuItem"

    name = attributes.text()
    parent = attributes.reference()

...we can see that items in two different stores cannot have references to one other...

>>> foo = MenuItem(store=s, name=u"foo")
>>> bar = MenuItem(store=ss, name=u"bar")
>>> foo.parent = bar
Traceback (most recent call last):
  File "/home/richard/workspace/Divmod/Axiom/axiom/attributes.py", line 566, in infilter
    raise NoCrossStoreReferences(
axiom.errors.NoCrossStoreReferences: You can't establish references to items in other stores.

...they can however find each other...

>>> [x.name for x in s.findFirst(substore.SubStore).open().query(MenuItem)]
[u'bar']
>>> ss.parent
<Store '/tmp/substoretest.axiom'@0xb784e2ec>
>>> [x.name for x in ss.parent.query(MenuItem)]
[u'foo']

Powerups

Powerups serve two main purposes:

  • separation of concern - they let an Item be used for things the original author never planned for;
  • they allow Items to be retrieved from the Store by reference to their Interface.

In the first instance the powerup can be thought of as a type of Axiom plugin.

One or more Powerups (Axiom Items) are registered to another Axiom Item in the same store or to the Store itself using the powerUp(pup, interface) method.

The Powerup(s) can be retrieved later by adaption (eg IMyInterface({Item,Store})) or by calling {Store,Item}.powerupsFor(IMyInterface).

In the former case the highest priority powerup is returned, in the latter, a generator of powerUps in order of priority.

Upgraders

Why

To illustrate the need for upgraders, we'll first create an IShopProduct interface and have ShopProduct power itself up accordingly so we can locate it via this interface later.

from axiom import item, attributes
from zope.interface import Interface, implements

class IShopProduct(Interface):
    """Marker interface for shop products"""

class ShopProduct(item.Item):
    implements(IShopProduct)

    typeName = "ShopProduct"

    name = attributes.text(allowNone=False)
    price = attributes.integer(allowNone=False)
    stock = attributes.integer(default=0)
    
    def installOn(self, other):
        other.powerUp(self, IShopProduct)

    def __repr__(self):
        return '<ShopProduct name="%s" price="%d" stock="%d">' % (self.name, self.price, self.stock)

So, we'll install some of these in a store:

s = Store('shop.axiom')
ShopProduct(name=u'streaky bacon', price=10, store=s).installOn(s)
ShopProduct(name=u'shoes', price=100000, store=s).installOn(s)

At some point in the future we think it would be a good idea to add a 'description' attribute to ShopProduct. Being reckless and indiscriminate, we just go ahead and stuff this on the class, at the same time adding 'schemaVersion' with a value of 2 (it defaulted to 1 before) because we are indeed updating our schema.

class ShopProduct(item.Item):
    implements(IShopProduct)

    typeName = "ShopProduct"
    schemaVersion = 2

    name = attributes.text(allowNone=False)
    price = attributes.integer(allowNone=False)
    stock = attributes.integer(default=0)
    description = attributes.text()
    
    def installOn(self, other):
        other.powerUp(self, IShopProduct)

    def __repr__(self):
        return '<ShopProduct name="%s" price="%d" stock="%d" description="%s">' % (self.name, self.price, self.stock, self.description)

We then try to load some ShopProducts:

from axiom.store import Store
from shopthings import IShopProduct

store = Store('yourstore.axiom')
for product in store.powerupsFor(IShopProduct):
    print product.description

but we get an error! What about all the version 1 ShopProducts?! There needs to be some kind of code telling those objects what to do with themselves in order to comform to the new schema. None of them have 'description' attributes, but from now on we expect all ShopProducts to have at least some description. The code that informs Axiom about what course of action to take is called an upgrader. So, we will write one.

How

Here is an example to upgrade ShopProducts.

def upgradeShopProduct1to2(oldShopProduct):
    if 'bacon' in oldShopProduct.name:
        description = 'salty'
    else:
        description = 'who cares'
    # this method is going to return another ShopProduct item,
    # with a schema version of two.  we pass in a new value for
    # the newly added 'description' attribute, and we copy the
    # others over from the old item
    return oldShopProduct.upgradeVersion('ShopProduct', 1, 2,
                                          description=description,
                                          name=oldShopProduct.name,
                                          price=oldShopProduct.price,
                                          stock=oldStockProduct.stock)

from axiom.upgrade import registerUpgrader
# we are telling axiom: "the upgradeShopProduct1to2 function should be used to
# upgrade items with a type name of 'ShopProduct' from version one to version two"
registerUpgrader(upgradeShopProduct1to2, 'ShopProduct', 1, 2)

If upgrading to higher version number (version>2) use the axiom.item.declareLegacyItem to provide axiom with the needed schema for that version of the item.

todo: write more stuff

jethro@divmod.org