Why another ORM?¶
From all modern frameworks like Django, SQLAlchemy , PonyORM and even peewee we have many beautiful instruments to perform database tasks. They cover wide area of database management problems, each focused on specific approach.
But, let’s say I have my own way of beautiful vision of how the things should be the way I love to.
Models definition¶
Use dataclasses¶
Why don’t use typing templates from python bundled dataclasses?
class Measure(DBTable):
name: str
weight: float = 1.0
class Product(DBTable):
name: str
base_measure: Measure
description: str = None
It has enough information about types and default values. Isn’t it?
Subclasses¶
It is very handy to declare strictly one-two-many related tables as a subclass.
class Product(DBTable):
name: str
base_measure: Measure
description: str = None
class Measure(DBTable):
measure: Measure
cnt: int
Many-to-many¶
Let’s make many-to-many relation definition simpler:
class User(NamedTable):
apps: ManyToMany[App]
class App(NamedTable):
users: ManyToMany[User]
or similar for one-to-many relation:
class User(NamedTable):
app: 'App'
class App(NamedTable):
users: Many[User] # this meta-fields would be provided implicitly
JSON fields¶
I like simple migrations for object-oriented databases. So, let’s decribe JSONb content fields explicitly.
class Journal(NamedTable):
body: FieldBody
title: Property[str]
price: Property[float]
group: Property[GroupCatalog]
pub_date: Property[datetime]
Here is body column typed JSONb in the table Journal. And other fields are just content in it.
Polymorphic entities¶
I like to store similar entities in one table, but separate logically.
class Catalog(DBTable):
_extendable_ = True
cid: FieldCID[str]
name: str
class ItemCatalog(Catalog):
unit: Unit
class GroupCatalog(Catalog):
pass
Enumerated types¶
I like Enum, IntEnum and StrEnum features.
class Journal(NamedTable):
class ContentClass(IntEnum):
MEDIUM = 1
BLOG = 2
HIGHLIGHTS = 3
cc: Journal.ContentClass
Queries¶
Lambdas way¶
Select fields by names or calculate by lambdas:
query = db.query(Item).select("name", "base_unit", unit=lambda x: x.base_unit.name)
print(query.fetch_all())
Filter by lambdas:
last_date = datetime.now() - timedelta(days=7)
query = db.query(News).filter(lambda x: x.created_at >= last_date).select('title')
for title in query.fetch_list():
print(title)
Precompiled queries¶
Let’s use Postgres binary protocol to precompile and reuse queries.
with db.query() as q, q.get_scheme() as s: # this block runs once, query `q` is cached for miltiple run
q.reuse()
q.select(date=s.sales.date, date_sum=q.sum(s.sales.rows.qty * s.sales.rows.unit.weight))
q.sort_by(2)
q.filter(s.sales.date >= day1 - timedelta(days=5))
q.filter(q.fields['date_sum'] > 80)
with db.select(q) as res:
for row in res:
print(row)
Note
We use a trick here. According to Python documentation, it is not explicitly said about context variable usage outside with context. But, certainly, we can.
Subqueries¶
Reuse initial query for additional results for easy
with db.query() as q2:
sub = q2.with_query(q)
q2.select(total_max=q2.max(sub.date_sum))
max_sum = q2.fetch_one().total_max
Queries result types¶
Let’s collect information about query result fields before actual execution. We would really need it to prepare user intefrace in advance.
fields: list[DBField] = query.describe()
for f in fields:
print(f'{f.name} - {f.type.__name__}')
Migrations¶
Do you have growing database? No problem, flexible migrations bundled.
db = DBFactory.postgres(conninfo="postgresql://quazy:quazy@localhost/quazy")
db.bind_module()
diff = compare_schema(db)
apply_changes(db, diff, 'my new migration')
IDE friendly¶
Just make stub file to provide little help to your IDE and other works will be performed by magic Generics.
with open("test_quazy.pyi", "wt") as f:
f.write(gen_stub(db))
It redefines all implicit fields explicitly and adds constructors.
Data validation¶
Make sure you put valid datas in your tables before database engine becomes confused.
class User(DBTable):
name: str
User(name=123)
# quazy.exceptions.QuazyFieldTypeError: Field `name` in `User` has wrong type: 1 validation error for str
# Input should be a valid string [type=string_type, input_value=123, input_type=int]
Asyncio support¶
Use benefits of concurrent coding right out of the box.
import asyncio
import random
from quazy import DBFactoryAsync, DBTable
class Product(DBTable):
name: str
price: float
description: str = None
async def main():
db = DBFactoryAsync.postgres("postgresql://quazy:quazy@127.0.0.1/quazy")
db._debug_mode = True
db.bind_module()
await db.clear()
await db.create()
for i in range(100):
await db.insert(Product(name=f'Product #{i + 1}', price=random.randint(1, 1000) / 100))
q = Product.query().filter(lambda x: x.price >= 5)
print("Total amount:", await q.fetch_count())
print("Average price:", await q.fetch_avg("price"))
print("Products:")
async for x in q:
print(x.name, "->", x.price)
if __name__ == "__main__":
asyncio.run(main())
Note
SQLite support
To run SQLite connection in async mode please install aiosqlite module.