The Axiom Lightning Tutorial
Hi. I'm glyph.
This is the tutorial that I wrote when I realized that Axiom had no documentation beyond a few blog posts, and nothing at all introductory. It was written fast and I expect you to read it fast. Luckily, Axiom is pretty simple.
What is Axiom?
Axiom is a relational object database for storing Python objects.
It is not simply an ORM. It is both less and more than an ORM. Less, because it supports only one database (SQLite). More, because it totally manages the database. You can schedule timed calls with it and run Twisted services with it. It comes with management and testing tools, and it does automatic schema migration.
Prerequisites
Install the Divmod code and Twisted. The easiest way to do this is to install combinator and learn how to use it. You are, however, welcome to do this however you like, as long as you can import axiom afterward.
You will also need some passing familiarity with SQL. Not a lot, as Axiom provides a subset of the full power of SQL in its query language, but Axiom is not an object database, so you will need some familiarity with the relational model.
First Bank of Exampletown
For my first example, I will write a very simple banking application; we will allow people to open accounts, make deposits and withdrawals.
The Basics
Defining an Item Type
Pooh sat down on a large stone, and tried to think this out. It sounded to him like a riddle, and he was never much good at riddles, being a Bear of Very Little Brain.
An Item is the basic unit of storage in Axiom. Item roughly corresponds to a record or row in relational database-speak and Item attributes correspond to columns.
Let's define an item type with a simple attribute.
# examplebank.py
from axiom.item import Item
from axiom.attributes import text
class Account(Item):
name = text()
This is already a full-fledged Axiom library. We have defined a new item type, examplebank.Account, which has a "name" attribute, which can only be text.
Note: "text" attributes in Axiom must always be unicode strings.
If you are familiar with SQL databases, what we've done here is similar in many ways to
CREATE TABLE account (name VARCHAR);
It's not exactly the same, but it gives you the vague idea.
Storing Data
For a little while Pooh couldn't think of anything. Then he thought: "Well, it's a very nice pot, even if there's no honey in it, and if I washed it clean, and got somebody to write 'A Happy Birthday' on it, Eeyore could keep things in it, which might be Useful."
Now let's use an interactive Python prompt to open a database, so that we can make an account and put one in to it. A Store is a container for Items, either in memory or on disk.
Python 2.5.1 (r251:54863, Mar 7 2008, 04:10:12)
[GCC 4.1.3 20070929 (prerelease) (Ubuntu 4.1.2-16ubuntu2)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from axiom.store import Store
>>> import examplebank
>>> s = Store("examplebank.axiom")
>>> account = examplebank.Account(store=s, name=u"John Smith")
>>> account
Account(name=u'John Smith', storeID=1)@0x855AEAC
We have now created an account that has been stored in an example database, on disk. If you inspect the folder where you ran this example, you will see that there is a folder named "examplebank.axiom", which contains the sqlite database.
Creating an Account object with a Store set to a particular store puts it into that database. The expression examplebank.Account(store=s, name=u"John Smith") is comparable to the SQL statement:
INSERT INTO account VALUES ("John Smith");
Retrieving Data
"Look, Piglet!" And as Piglet looked sorrowfully round, Eeyore picked the balloon up with his teeth, and placed it carefully in the pot; picked it out and put it on the ground; and then picked it up again and put it carefully back. "So it does!" said Pooh. "It goes in!" "So it does!" said Piglet. "And it comes out!" "Doesn't it?" said Eeyore. "It goes in and out like anything."
We've stored some data. Now we want to get it out again. Let's use Store's "query" method to retrieve it:
>>> list(s.query(examplebank.Account)) [Account(name=u'John Smith', storeID=1)@0x855AEAC]
There's our account, owned by Mr. John Smith.
Again, if you're familiar with SQL databases already, this is simply an equivalent to:
SELECT * from account;
Whereas in SQL we would use the table's name, here in Axiom we have used the class object. s.query(Account) indicates that we want results of the type Account yielded from the query.
N.B. Store.query returns an iterator. Getting a list of Items using list(s.query(...)) is expedient and useful for interactive development and testing. However, when the query may return a large number of results you probably don't want to call list() on it; instead consider iterating over it yourself and dealing with each Item as needed.
Other Data Types
Let's revisit our application. An account doesn't simply have a name, it has a balance.
# examplebank.py
from axiom.item import Item
from axiom.attributes import text, integer
class Account(Item):
name = text()
balance = integer(default=0)
We can specify that the default value for the "balance" attribute is zero, since new accounts start with no money in them.
Now we've defined a second attribute, but this still isn't enough. We don't want to let people overdraw their balance, so let's give our account some methods.
Methods on Items
# examplebank2.py
from axiom.item import Item
from axiom.attributes import text, integer
class Overdraft(Exception):
"The account would have been overdrawn!"
class Account(Item):
name = text()
balance = integer(default=0)
def withdraw(self, amount):
if amount > self.balance:
raise Overdraft()
self.balance -= amount
def deposit(self, amount):
self.balance += amount
In terms of SQL, the methods do not exist, but this modified class statement is now like:
CREATE TABLE account (name VARCHAR, balance INTEGER);
Let's head over to a Python prompt to play with the new, improved Account.
Python 2.5.1 (r251:54863, Mar 7 2008, 04:10:12)
[GCC 4.1.3 20070929 (prerelease) (Ubuntu 4.1.2-16ubuntu2)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from axiom.store import Store
>>> from examplebank2 import Account
>>> s = Store('examplebank2.axiom')
>>> alice = Account(store=s, name=u'alice')
>>> bob = Account(store=s, name=u'bob')
>>> carol = Account(store=s, name=u'carol')
>>> alice.deposit(1000)
>>> bob.deposit(200)
>>> alice
Account(balance=1000, name=u'alice', storeID=3)@0x856026C
>>> bob
Account(balance=200, name=u'bob', storeID=4)@0x856038C
>>> carol
Account(balance=0, name=u'carol', storeID=5)@0x856056C
>>> bob.withdraw(20000)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "examplebank.py", line 14, in withdraw
raise Overdraft()
examplebank.Overdraft
As you can see, these methods are normal-looking Python code that simply access the column result values as attributes. You call them by simply calling methods on an object.
The attribute update calls to withdraw and deposit can be interpreted as SQL statements like these, roughly speaking:
UPDATE account SET balance = balance + 1000 WHERE name = "alice"; UPDATE account SET balance = balance + 200 WHERE name = "bob";
Simple Queries
We've already retrieved some data, but let's retrieve specific data. What if we want all the customers of the bank who have some money in their accounts?
>>> list(s.query(Account, Account.balance > 0)) [Account(balance=1000, name=u'alice', storeID=3)@0x856026C, Account(balance=200, name=u'bob', storeID=4)@0x856038C]
Previously we used the Account object to indicate a table. Now we're using one of its attributes, Account.balance, to indicate a column. We are passing an expression, Account.balance > 0 to query, saying that we want all accounts whose balance is more than zero.
This is comparable to the SQL statement:
SELECT * FROM account WHERE account.balance > 0;
Complex Queries
You can get more advanced with this query as well. For example, what if we wanted to retrieve all the members of the bank who had a balance of more than zero, but less than five hundred?
>>> from axiom.attributes import AND >>> list(s.query(Account, AND(Account.balance > 0, Account.balance < 500))) [Account(balance=200, name=u'bob', storeID=4)@0x856038C]
Note: 'and' and 'or' in Python don't work the same as other operators, so we can't use them to make this syntax prettier.
As you might already have guessed, this corresponds to SQL:
SELECT * FROM account WHERE account.balance > 0 AND account.balance < 500;
References between Items
(Hi, I'm Allen. Glyph asked me to add some more stuff to this tutorial.)
Of course, it's possible for a bank customer to have more than one account. Let's revise our example once more, to track Customers separately from Accounts:
# examplebank3.py
from axiom.item import Item
from axiom.attributes import text, integer, reference
class Overdraft(Exception):
"The account would have been overdrawn!"
class Customer(Item):
name = text()
def newAccount(self, accountName):
return Account(store=self.store, name=accountName, owner=self)
class Account(Item):
owner = reference()
name = text()
balance = integer(default=0)
def withdraw(self, amount):
if amount > self.balance:
raise Overdraft()
self.balance -= amount
def deposit(self, amount):
self.balance += amount
Here we've added a new Item type, Customer, and a new attribute on Account, "owner". The "reference" attribute type can hold a reference to another Item in the same store. So Account's definition is almost (but not quite) like the following SQL (we'll talk about how it's different in the next section):
CREATE TABLE account (owner INTEGER FOREIGN KEY REFERENCES Customer(id), name VARCHAR, balance INTEGER);
And so now we can create multiple accounts that refer to their owning Customer.
Python 2.5.2 (r252:60911, Mar 12 2008, 13:36:25)
[GCC 4.2.3 (Ubuntu 4.2.3-2ubuntu4)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from axiom.store import Store
>>> from examplebank3 import Account, Customer
>>> s = Store("examplebank3.axiom")
>>> alice = Customer(store=s, name=u'alice')
>>> checking = alice.newAccount(u'checking account')
>>> savings = alice.newAccount(u'savings account')
>>> alice
Customer(name=u'alice', storeID=1)@0x86A194C
>>> checking.owner
Customer(name=u'alice', storeID=1)@0x86A194C
>>> savings.owner
Customer(name=u'alice', storeID=1)@0x86A194C
Now quite often in a Python application if a customer could have more than one account we would keep a list of Accounts as an attribute on the Customer object, but this is a relational database, so it's enough for the Account to have a reference to its owner; we can run a query to find all the Accounts belonging to a single Customer.
>>> list(s.query(Account, Account.owner == alice)) [Account(balance=0, name=u'checking account', owner=reference(1), storeID=2)@0x86A1A6C, Account(balance=0, name=u'savings account', owner=reference(1), storeID=3)@0x86A1DAC]
Again, this is kind of like the SQL query:
SELECT * FROM account WHERE account.owner = <ID_for_Customer_alice>
Advanced References (Dynamic Typing)
The "reference" attribute type is more flexible than the foreign-key reference mentioned above because it can refer to any Item type. For example, suppose we want to add a new BusinessCustomer item to store information about accounts owned by businesses separately from individuals. (Obviously this new item type is not very different, but the main point here is that it's unrelated to our original Customer class -- we can write different methods and attribute types and so forth for it as we please.)
class BusinessCustomer(Item):
businessName = text()
def newAccount(self, accountName):
return Account(store=self.store, name=accountName, owner=self)
We can simply add the new type and immediately create new Accounts that refer to the new type of item.
>>> from examplebank3 import BusinessCustomer >>> bc = BusinessCustomer(store=s, businessName=u"Globodyne Inc") >>> account = bc.newAccount(u"payroll account") >>> account.owner BusinessCustomer(businessName=u'Globodyne Inc', storeID=4)@0x87244EC >>> account Account(balance=0, name=u'payroll account', owner=reference(4), storeID=5)@0x872154C
So now we have Account items whose "owner" field refers to items of different types. This is possible because behind the scenes Axiom uses a separate table for keeping track of each item's type and table row index. Note that each item has a "storeID" attribute -- references are done via this store ID, which is unique for each item. We can also directly retrieve items by this ID, if necessary:
>>> s.getItemByID(4) BusinessCustomer(businessName=u'Globodyne Inc', storeID=4)@0x87244EC >>> s.getItemByID(4) is bc True
Super Advanced References (Powerups)
Axiom also has a form of object composition called "powerups", by which additional behaviour or data can be added to a specific database item. This composition is done with the aid of zope.interface; specifically, a powerup item is associated with the item being "empowered" by an Interface that describes the powerup's behaviour.
Our original Account example raised an error if a withdrawal was attempted that would reduce the balance below zero. This isn't the only way to handle things; many banks offer overdraft protection as an additional service for their customers. This can be done by drawing from another bank account or a line of credit to cover the shortfall. Let's add that to our example.
#examplebank4.py
from axiom.item import Item
from axiom.attributes import text, integer, reference
from zope.interface import Interface, implements
class IOverdraftProtection(Interface):
"A means to cover overdrafts on an account"
def coverOverdraft(amount):
"Cover an overdraft of the specified amount."
class Overdraft(Exception):
"The account would have been overdrawn!"
class LineOfCredit(Item):
implements(IOverdraftProtection)
creditLimit = integer()
balance = integer()
def coverOverdraft(self, amount):
if amount + self.balance > creditLimit:
raise Overdraft()
self.balance += amount
class AccountLink(Item):
implements(IOverdraftProtection)
linkedAccount = reference()
def coverOverdraft(self, amount):
self.linkedAccount.withdraw(amount)
class Customer(Item):
name = text()
def newAccount(self, accountName):
return Account(store=self.store, name=accountName, owner=self)
class Account(Item):
owner = reference()
name = text()
balance = integer(default=0)
def withdraw(self, amount):
if amount > self.balance:
self.handleOverdraft(amount)
else:
self.balance -= amount
def deposit(self, amount):
self.balance += amount
def handleOverdraft(self, amount):
overdraftAmount = amount - self.balance
for x in self.powerupsFor(IOverdraftProtection):
try:
self.balance = 0
x.coverOverdraft(overdraftAmount)
break
except Overdraft:
continue
else:
raise Overdraft()
We start by adding an interface, IOverdraftProtection, to describe the kind of new behaviour we want. We then add two Items that implement this interface, AccountLink and LineOfCredit; one draws money from an existing deposit, and one borrows money. We can then create two accounts and link them for overdraft protection:
Python 2.5.2 (r252:60911, Mar 12 2008, 13:36:25)
[GCC 4.2.3 (Ubuntu 4.2.3-2ubuntu4)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from examplebank4 import Account, Customer, AccountLink, IOverdraftProtection
>>> from axiom.store import Store
>>> s = Store("examplebank4.axiom")
>>> alice = Customer(store=s, name=u"Alice")
>>> acct1 = alice.newAccount(u"checking account")
>>> acct2 = alice.newAccount(u"savings account")
>>> link = AccountLink(store=s, linkedAccount=acct2)
>>> acct1.powerUp(link, IOverdraftProtection)
Calling powerUp on an Item installs another item as a powerup for the given interface.
>>> acct2.deposit(100) >>> acct1.withdraw(10) >>> acct2.balance 90
So now when withdrawals are made that would cause an overdraft, Item.powerupsFor is called to find powerups for IOverdraftProtection, and overdraft coverage is requested from each until one succeeds. You can install as many powerups as you like on an item, but installing only one is a common case; for convenience, an item can be adapted to one of its powerup objects by calling the interface with the item. For example:
>>> IOverdraftProtection(acct1) AccountLink(linkedAccount=reference(8), storeID=9)@0x83E756C
In this case, however, there's value in installing a second powerup: if there's not enough money in the linked account, the line of credit will be drawn upon.
>>> acct1.withdraw(110)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "examplebank4.py", line 43, in withdraw
self.handleOverdraft(amount)
File "examplebank4.py", line 59, in handleOverdraft
raise Overdraft()
examplebank4.Overdraft
>>> from examplebank4 import LineOfCredit
>>> loc = LineOfCredit(store=s, creditLimit=200)
>>> acct1.powerUp(loc, IOverdraftProtection)
>>> acct1.withdraw(110)
>>> loc.balance
110
Topics Not Yet Covered
Changing Your Schema
Writing Upgraders
Running Upgraders
Testing Upgraders
Run the service, wait for whenFullyUpgraded
Upgrader Caveats
Query Inconsistency
getItemByID
Starting Axiomatic
Using the Scheduler (Build Your Own clow^H^H^Hron)
One of Axiom's more unique features is its integrated scheduler. Since Axiom is designed for use with Twisted, it takes advantage of its event loop to run tasks at specified times.
To create a scheduled task, you need an item that has a "run" method (which here we'll call a "runnable"). Let's create an example that simply prints a message to stdout when it runs:
#reminder.py
from axiom.item import Item
from axiom.attributes import text
class TaskReminder(Item):
message = text()
def run(self):
print "Reminder: %s" % (self.message.encode('ascii', 'replace'),)
Now let's create a store and put a Scheduler in it (XXX there is a better way to set this up):
Python 2.5.2 (r252:60911, Mar 12 2008, 13:36:25)
[GCC 4.2.3 (Ubuntu 4.2.3-2ubuntu4)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> from reminder import TaskReminder
>>> from axiom.store import Store
>>> from axiom.scheduler import Scheduler
>>> from axiom.iaxiom import IScheduler
>>> from twisted.application.service import IService
>>> s = Store("reminder.axiom")
>>> scheduler = Scheduler(store=s)
>>> s.powerUp(scheduler, IScheduler)
>>> s.powerUp(scheduler, IService)
Now we can create a task, and schedule it.
>>> from epsilon.extime import Time >>> from datetime import timedelta >>> now = Time() >>> t = TaskReminder(store=s, message=u"Giant robot sale at Fry's starts in an hour!") >>> when = now + timedelta(minutes=5) >>> print when.asHumanly() 12:48 am >>> scheduler.schedule(t, when)
For this scheduled task to run, though, we must leave the interactive interpreter and start the Twisted event loop using axiomatic as described above.
% axiomatic -d reminder.axiom start -n 2008-04-01 19:43:53-0500 [-] Log opened. 2008-04-01 19:43:53-0500 [-] twistd 8.0.1+r23107 (/usr/bin/python 2.5.2) starting up 2008-04-01 19:43:53-0500 [-] reactor class: <class 'twisted.internet.selectreactor.SelectReactor'> 2008-04-01 19:48:18-0500 [-] Reminder: Giant robot sale at Fry's starts in an hour!
(You can stop the axiomatic process with Ctrl-C.)
Note that the task ran 5 minutes after it was scheduled (now + timedelta(minutes=5)), rather than 5 minutes after the server was started.
Runnable items can be queried for and manipulated as any other database item, and can be rescheduled to run as many times as needed. To remove a task from the scheduler, Scheduler.unscheduleFirst(runnable) or Scheduler.unscheduleAll(runnable) can be called, depending on whether removing only the next scheduled run or all of them is desired.
