cuneiform is the result of a Learning Day @ solute; we tried to build an ORM in a day.
cuneiform is relatively self-contained, it just needs psycopg2 installed and a postgres database.
Start by importing cuneiform and configuring the database connection:
import cuneiform as cf cf.configure(db="cuneiform", user="cuneiform", password="cuneiform")
You can then start defining models. Lets start with a simple CRM for no reason whatsoever:
from enum import Enum class CompanyType(Enum): Ltd = 1 Inc = 2 SE = 3 AB = 4 GmbH = 5 class Company(cf.Model): name = cf.Field(str) type = cf.Field(CompanyType)
As you can see, you define a model by subclassing
cf.Model and declare its
cf.Field(some_type). For simple columns with one out of a small
number of values, you can use an Enum, which gets translated into an int column
internally (but cuneiform will make sure you only assign e.g.
instances to it).
Cuneiform will now automatically create the necessary table and we can start inserting some rows:
>>> solute = Company(name="solute", type=CompanyType.GmbH) >>> solute <Company[D] name='solute' type=CompanyType.GmbH>
As you can see, the resulting object was marked with the "dirty" flag (
meaning it was not yet written to the database. To do that, you call
We can now also retrieve this instance by searching for it in various ways:
>>> rs = Company.select(where=Company.name == "solute") >>> rs = Company.select(where=Company.type == CompanyType.GmbH) >>> rs = Company.select(where=(Company.type == CompanyType.GmbH) & (Company.name == "solute"))
All these queries return the same thing: a lazy recordset describing the
eventual query to be made. To actually return instances, we can iterate over
them (or call
list()). In case we know there can only be one row, we can
>>> list(rs) [<Company name='solute' type=CompanyType.GmbH>] >>> rs.get() <Company name='solute' type=CompanyType.GmbH>
Recordsets also support limits and orderings. All of these can be added in the
.select() call, or later with methods that return another RecordSet.
The following lines are all equivalent:
>>> rs = Company.select(where=Company.name == "solute", order_by=Company.name.asc, limit=23) >>> rs = Company.select(where=Company.name == "solute").order_by(Company.name.asc).limit(23) >>> rs = Company.select().order_by(Company.name.asc).limit(23).filter(Company.name == "solute")
Finally, in addition to retrieving objects from a record set, you can also
perform bulk operations like deletions (with
.delete()) and updates (e.g.
We now have Objects that are Mapped into a database, but no relationality yet. Let's define another model:
class Address(cf.Model): street = cf.Field(str) house = cf.Field(int) post_code = cf.Field(str) town = cf.Field(str)
In order to link these together, lets add a new column to our
class Company(cf.Model): name = cf.Field(str) type = cf.Field(CompanyType) addr = cf.Field(Address)
Make sure to define
Company so you don't get a NameError.
Cuneiform will automatically take care of adding the new column to the
company table and setting up a foreign key relation. Lets augment our existing customer:
>>> solute = Company.select(where=Company.name=="solute") >>> address = Address(street="Zeppelinstraße", house=15, post_code="76185", town="Karlsruhe") >>> solute.addr = address >>> solute.save()
.save() method recursively makes sure that all dependent objects are
saved as well, so we don't have to explicitly save the address.
Lets see what querying possibilities we have gained:
>>> Company.select(where=Company.addr.town=="Karlsruhe").get() <Company name='solute' type=CompanyType.GmbH> >>> address.companies.get() <Company name='solute' type=CompanyType.GmbH>
companies? Cuneiform automatically adds reverse relations to the
referenced models as well, defaulting to the plural form of the source model.
address.companies is a RecordSet containing all companies that have this
Quirks and small features
- Because it is impossible to override the
oroperators in Python, we had to resort to using
|. Since they have a much stronger precedence than the textual versions, you always need to paranthesize your inner expressions when combining them like this. Thankfully, we can at least make sure you do so, because we define
__rand__on the Field class.
- Recordsets also support length querying via
- In its current state, cuneiform is very brutal when it comes to model changes. It will delete and recreate your tables or columns without hesitation when it thinks it needs to. Think of it as a warning not to actually use this anywhere serious.