Stellata is a simple, but fully-featured PostgreSQL ORM for Python 3.
To connect to a database on the default host/port with a threaded connection pool of size 10:
import stellata.database
db = stellata.database.initialize(
name='database',
user='user',
password='password',
host='localhost',
port=5432,
pool_size=10
)
A user model might look something like this:
import stellata.fields
import stellata.model
class User(stellata.model.Model):
__table__ = 'users'
id = stellata.fields.UUID(null=False)
active = stellata.fields.Integer(default=1)
name = stellata.fields.Text(null=False)
hash = stellata.fields.Varchar(length=255)
email = stellata.fields.Text()
dt = stellata.fields.Timestamp(null=False)
The __table__
property is the name of the corresponding table in the PostgreSQL database. Each property corresponds to a column, and the classes in stellata.fields
are used to define the column types.
- BigInteger
- Boolean
- Integer
- Numeric
- Text
- Timestamp
- UUID
- Varchar
Like any good ORM, Stellata supports relations among models. Here are two related models, A
and B
:
import stellata.model
import stellata.fields
import stellata.relations
class A(stellata.model.Model):
__table__ = 'a'
id = stellata.fields.UUID()
foo = stellata.fields.Text()
bar = stellata.fields.Integer(default=0, null=False)
b = stellata.relations.HasMany(lambda: B.a_id)
class B(stellata.model.Model):
__table__ = 'b'
id = stellata.fields.UUID()
a_id = stellata.fields.UUID()
bar = stellata.fields.Integer(null=False)
a = stellata.relations.BelongsTo(lambda: B.a_id, lambda: A)
Indexes make your queries go fast. Let's add a couple indexes to our table:
import stellata.model
import stellata.fields
import stellata.index
import stellata.relations
class A(stellata.model.Model):
__table__ = 'a'
id = stellata.fields.UUID()
foo = stellata.fields.Text()
bar = stellata.fields.Integer(default=0, null=False)
b = stellata.relations.HasMany(lambda: B.A_id, lambda: A)
primary_key = stellata.index.PrimaryKey(lambda: A.id)
foo_index = stellata.index.Index(lambda: A.foo, unique=True)
Once you have some data, it won't be long until you want to convert it to JSON. To do so, use:
stellata.model.serialize(model)
This will recursively serialize objects/relations, and you can pass it an object, dictionary, list, etc.
In some cases, it's handy to be able to iterate over all of the models you've defined. For example, you might want to truncate tables for a unit test. In that case, you can do this:
for model in stellata.model.registered():
# do something with model
Once you've defined your models, you can sync them with your database by performing a migration.
stellata.schema.migrate(db, execute=True)
Here, db
is the handle returned by the stellata.database.initialize
call. If you'd like to do a dry run, without actually executing any queries, do:
stellata.schema.migrate(db)
In both cases, this function will return a list of queries needed for the migration.
In some development scripts, you might want to clean your database. If you so desire, you can do this:
stellata.schema.drop_tables_and_lose_all_data(db, execute=True)
As its name suggests, this function is very destructive, so don't do this on a production database.
Finally, let's walk through how to use Stellata to query your database.
Let's create a new instance of A
.
a = A.create(A(foo='bar', bar=5))
a.id == '2a12f545-c587-4b99-8fd2-57e79f7c8bca'
a.foo == 'bar'
a.bar == 4
Or, if we want to create in bulk:
result = A.create([
A(foo='bar', bar=6),
A(foo='baz', bar=7)
])
len(result) == 2
If you created a unique index on some fields, you can take advantage of the PostgreSQL ON CONFLICT feature:
A.create(A(foo='baz', bar=9), unique=(A.foo,))
Now, if there's already a row with foo
having a value of baz
, then the bar
column will be updated to have a value of 9
, rather than creating a new row.
To read from the database, we'll want to use the where
method. Let's get the instance of A
we created before:
a = A.where(A.id == '2a12f545-c587-4b99-8fd2-57e79f7c8bca').get()
a.id == '2a12f545-c587-4b99-8fd2-57e79f7c8bca'
Jeez Rick, what's that syntax? We're using operator overloading, Morty. What else can we do?
A.where(A.bar < 5).get()
A.where(A.bar > 1).get()
A.where(A.id << ['2a12f545-c587-4b99-8fd2-57e79f7c8bca', '31be0c81-f5ee-49b9-a624-356402427f76']).get()
That last one is a where in query, in case that wasn't—burp—obvious. We can also use AND and OR in our queries like so:
A.where((A.id == '2a12f545-c587-4b99-8fd2-57e79f7c8bca') | (A.bar < 5)).get()
A.where((A.id == '2a12f545-c587-4b99-8fd2-57e79f7c8bca') & (A.bar > 1)).get()
Other bells and whistles:
A.where(A.bar < 5).order(A.bar, 'asc').limit(5).get()
A common read operation is to find all rows where a column matches some value, so we can use a shorthand:
A.find('2a12f545-c587-4b99-8fd2-57e79f7c8bca')
By default, the id
field will be used, but you can also specify your own field:
A.find(5, A.bar)
If given a list, find
will return a dictionary keyed on the value of the field you specify:
a = A.find(['2a12f545-c587-4b99-8fd2-57e79f7c8bca', '31be0c81-f5ee-49b9-a624-356402427f76'])
a['2a12f545-c587-4b99-8fd2-57e79f7c8bca'].id == '2a12f545-c587-4b99-8fd2-57e79f7c8bca'
a['31be0c81-f5ee-49b9-a624-356402427f76'].id == '31be0c81-f5ee-49b9-a624-356402427f76'
We can use those relations we set up earlier with joins. Let's say we create the following:
a = A.create(A(foo='bar', bar=5))
a.id == '2a12f545-c587-4b99-8fd2-57e79f7c8bca'
b = B.create([
B(a_id='2a12f545-c587-4b99-8fd2-57e79f7c8bca', qux=3)
B(a_id='2a12f545-c587-4b99-8fd2-57e79f7c8bca', qux=5)
])
Now, we can do this:
a = A.join(A.b).where(A.id == '2a12f545-c587-4b99-8fd2-57e79f7c8bca').get()
a.id == '2a12f545-c587-4b99-8fd2-57e79f7c8bca'
len(a.b) == 2
a.b[0].qux == 3
a.b[1].qux == 5
Or, the other way:
b = B.join(B.a).where(B.qux << [3, 5]).get()
len(b) == 2
b[0].qux == 3
b[0].a.id == '2a12f545-c587-4b99-8fd2-57e79f7c8bca'
b[1].qux == 5
b[1].a.id == '2a12f545-c587-4b99-8fd2-57e79f7c8bca'
By default, joins will be executed via multiple SELECT queries. If you'd prefer to do a JOIN instead, just do this:
a = A.join_with('join').join(A.b).where(A.id == '2a12f545-c587-4b99-8fd2-57e79f7c8bca').get()
The result is the same as before, but the underlying query was different. Which method you use is entirely up to you, and may vary with different queries.
As you might expect, update queries combine the syntax for creating and reading:
A.where(A.id == '2a12f545-c587-4b99-8fd2-57e79f7c8bca').update(A(bar=7))
This one is easy now.
A.where(id == '2a12f545-c587-4b99-8fd2-57e79f7c8bca').delete()
Or, if you prefer a single TRUNCATE operation:
A.truncate()
Stellata also has support for PostgreSQL transactions:
A.begin()
A.truncate()
A.create([A(bar=1), A(bar=2)])
A.commit()