ormar
Overview
The ormar package is an async mini ORM for Python, with support for Postgres,
MySQL, and SQLite.
The main benefits of using ormar are:
- getting an async ORM that can be used with async frameworks (fastapi, starlette etc.)
- getting just one model to maintain - you don't have to maintain pydantic and other orm models (sqlalchemy, peewee, gino etc.)
The goal was to create a simple ORM that can be used directly (as request and response models) with fastapi that bases it's data validation on pydantic.
Ormar - apart from the obvious "ORM" in name - gets its name from ormar in Swedish which means snakes, and ormar in Croatian which means cabinet.
And what's a better name for python ORM than snakes cabinet :)
If you like ormar remember to star the repository in github!
The bigger community we build, the easier it will be to catch bugs and attract contributors ;)
Documentation
Check out the documentation for details.
Note that for brevity most of the documentation snippets omit the creation of the database
and scheduling the execution of functions for asynchronous run.
If you want more real life examples than in the documentation you can see the tests folder,
since they actually have to create and connect to a database in most of the tests.
Yet remember that those are - well - tests and not all solutions are suitable to be used in real life applications.
Part of the fastapi ecosystem
As part of the fastapi ecosystem ormar is supported in libraries that somehow work with databases.
As of now ormar is supported by:
If you maintain or use a different library and would like it to support ormar let us know how we can help.
Dependencies
Ormar is built with:
License
ormar is built as open-sorce software and will remain completely free (MIT license).
As I write open-source code to solve everyday problems in my work or to promote and build strong python
community you can say thank you and buy me a coffee or sponsor me with a monthly amount to help ensure my work remains free and maintained.
Sponsor - Github Sponsors
Migrating from sqlalchemy and existing databases
If you currently use sqlalchemy and would like to switch to ormar check out the auto-translation
tool that can help you with translating existing sqlalchemy orm models so you do not have to do it manually.
Beta versions available at github: sqlalchemy-to-ormar
or simply pip install sqlalchemy-to-ormar
sqlalchemy-to-ormar can be used in pair with sqlacodegen to auto-map/ generate ormar models from existing database, even if you don't use sqlalchemy for your project.
Migrations & Database creation
Because ormar is built on SQLAlchemy core, you can use alembic to provide
database migrations (and you really should for production code).
For tests and basic applications the sqlalchemy is more than enough:
import sqlalchemy
import databases
DATABASE_URL = "sqlite:///db.sqlite"
database = databases.Database(DATABASE_URL)
metadata = sqlalchemy.MetaData()
engine = sqlalchemy.create_engine(DATABASE_URL)
metadata.create_all(engine)
For a sample configuration of alembic and more information regarding migrations and
database creation visit migrations documentation section.
Package versions
ormar is still under development:
We recommend pinning any dependencies (with i.e. ormar~=0.9.1)
ormar also follows the release numeration that breaking changes bump the major number,
while other changes and fixes bump minor number, so with the latter you should be safe to
update, yet always read the releases docs before.
example: (0.5.2 -> 0.6.0 - breaking, 0.5.2 -> 0.5.3 - non breaking).
Asynchronous Python
Note that ormar is an asynchronous ORM, which means that you have to await the calls to
the methods, that are scheduled for execution in an event loop. Python has a builtin module
asyncio that allows you to do just that.
Note that most "normal" python interpreters do not allow execution of await
outside of a function (because you actually schedule this function for delayed execution
and don't get the result immediately).
In a modern web framework (like fastapi), the framework will handle this for you, but if
you plan to do this on your own you need to perform this manually like described in the
quick start below.
Quick Start
Note that you can find the same script in examples folder on github.
from typing import Optional
import databases
import pydantic
import ormar
import sqlalchemy
DATABASE_URL = "sqlite:///db.sqlite"
base_ormar_config = ormar.OrmarConfig(
database=databases.Database(DATABASE_URL),
metadata=sqlalchemy.MetaData(),
engine=sqlalchemy.create_engine(DATABASE_URL),
)
class Author(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="authors")
id: int = ormar.Integer(primary_key=True)
name: str = ormar.String(max_length=100)
class Book(ormar.Model):
ormar_config = base_ormar_config.copy(tablename="books")
id: int = ormar.Integer(primary_key=True)
author: Optional[Author] = ormar.ForeignKey(Author)
title: str = ormar.String(max_length=100)
year: int = ormar.Integer(nullable=True)
base_ormar_config.metadata.drop_all(base_ormar_config.engine)
base_ormar_config.metadata.create_all(base_ormar_config.engine)
async def create():
tolkien = await Author.objects.create(name="J.R.R. Tolkien")
await Book.objects.create(author=tolkien, title="The Hobbit", year=1937)
await Book.objects.create(author=tolkien, title="The Lord of the Rings", year=1955)
await Book.objects.create(author=tolkien, title="The Silmarillion", year=1977)
sapkowski = Author(name="Andrzej Sapkowski")
await sapkowski.save()
await Book(author=sapkowski, title="The Witcher", year=1990).save()
await Book(author=sapkowski, title="The Tower of Fools", year=2002).save()
async def read():
book = await Book.objects.get(title="The Hobbit")
book = await Book.objects.get(Book.title == "The Hobbit")
book2 = await Book.objects.first()
assert book == book2
assert book.title == "The Hobbit"
assert book.year == 1937
book3 = await Book.objects.get()
assert book3.title == "The Tower of Fools"
assert isinstance(book.author, Author)
assert book.author.pk == 1
assert book.author.name is None
await book.author.load()
assert book.author.name == "J.R.R. Tolkien"
authors = await Author.objects.all()
assert len(authors) == 2
async def update():
tolkien = await Author.objects.get(name="J.R.R. Tolkien")
assert tolkien.name == "J.R.R. Tolkien"
tolkien_id = tolkien.id
tolkien.name = "John Ronald Reuel Tolkien"
await tolkien.update()
tolkien = await Author.objects.get(name="John Ronald Reuel Tolkien")
assert tolkien.name == "John Ronald Reuel Tolkien"
assert tolkien.id == tolkien_id
await Author.objects.filter(name__contains="Tolkien").update(name="J.R.R. Tolkien")
async def delete():
silmarillion = await Book.objects.get(year=1977)
await silmarillion.delete()
await Book.objects.delete(title="The Tower of Fools")
try:
await Book.objects.get(year=1977)
except ormar.NoMatch:
print("No book from 1977!")
tolkien = silmarillion.author
await Book.objects.create(author=tolkien, title="The Silmarillion", year=1977)
async def joins():
book = await Book.objects.select_related("author").get(title="The Hobbit")
book = await Book.objects.select_related(Book.author).get(
Book.title == "The Hobbit"
)
assert book.author.name == "J.R.R. Tolkien"
author = await Author.objects.select_related("books").all(name="J.R.R. Tolkien")
author = await Author.objects.select_related(Author.books).all(
Author.name == "J.R.R. Tolkien"
)
assert len(author[0].books) == 3
author = await Author.objects.prefetch_related("books").get(name="J.R.R. Tolkien")
author = await Author.objects.prefetch_related(Author.books).get(
Author.name == "J.R.R. Tolkien"
)
assert len(author.books) == 3
async def filter_and_sort():
books = await Book.objects.all(author__name="J.R.R. Tolkien")
books = await Book.objects.all(Book.author.name == "J.R.R. Tolkien")
assert len(books) == 3
books = await Book.objects.filter(author__name__icontains="tolkien").all()
books = await Book.objects.filter(Book.author.name.icontains("tolkien")).all()
assert len(books) == 3
books = (
await Book.objects.filter(author__name__icontains="tolkien")
.order_by("-year")
.all()
)
books = (
await Book.objects.filter(Book.author.name.icontains("tolkien"))
.order_by(Book.year.desc())
.all()
)
assert len(books) == 3
assert books[0].title == "The Silmarillion"
assert books[2].title == "The Hobbit"
async def subset_of_columns():
hobbit = await Book.objects.fields(["title"]).get(title="The Hobbit")
assert hobbit.year is None
assert hobbit.author is None
assert hobbit.title == "The Hobbit"
hobbit = await Book.objects.exclude_fields(["year"]).get(title="The Hobbit")
assert hobbit.year is None
assert hobbit.author is not None
assert hobbit.pk is not None
try:
await Book.objects.exclude_fields(["title"]).get(title="The Hobbit")
except pydantic.ValidationError:
print("Cannot exclude non nullable field title")
async def pagination():
books = await Book.objects.limit(1).all()
assert len(books) == 1
assert books[0].title == "The Hobbit"
books = await Book.objects.limit(1).offset(1).all()
assert len(books) == 1
assert books[0].title == "The Lord of the Rings"
books = await Book.objects.paginate(page=2, page_size=2).all()
assert len(books) == 2
assert books[0].title == "The Witcher"
assert books[1].title == "The Silmarillion"
async def aggregations():
assert 2 == await Author.objects.count()
assert await Book.objects.filter(title="The Hobbit").exists()
assert 1990 == await Book.objects.max(columns=["year"])
assert 1937 == await Book.objects.min(columns=["year"])
assert 1964.75 == await Book.objects.avg(columns=["year"])
assert 7859 == await Book.objects.sum(columns=["year"])
async def raw_data():
assert await Book.objects.values() == [
{"id": 1, "author": 1, "title": "The Hobbit", "year": 1937},
{"id": 2, "author": 1, "title": "The Lord of the Rings", "year": 1955},
{"id": 4, "author": 2, "title": "The Witcher", "year": 1990},
{"id": 5, "author": 1, "title": "The Silmarillion", "year": 1977},
]
assert await Book.objects.values_list() == [
(1, 1, "The Hobbit", 1937),
(2, 1, "The Lord of the Rings", 1955),
(4, 2, "The Witcher", 1990),
(5, 1, "The Silmarillion", 1977),
]
assert await Book.objects.filter(title="The Hobbit").values() == [
{"id": 1, "author": 1, "title": "The Hobbit", "year": 1937}
]
assert await Book.objects.filter(title="The Hobbit").values(["id", "title"]) == [
{"id": 1, "title": "The Hobbit"}
]
assert await Book.objects.values_list("title", flatten=True) == [
"The Hobbit",
"The Lord of the Rings",
"The Witcher",
"The Silmarillion",
]
async def with_connect(function):
async with base_ormar_config.database:
await function()
import asyncio
for func in [
create,
read,
update,
delete,
joins,
filter_and_sort,
subset_of_columns,
pagination,
aggregations,
raw_data,
]:
print(f"Executing: {func.__name__}")
asyncio.run(with_connect(func))
base_ormar_config.metadata.drop_all(base_ormar_config.engine)
Ormar Specification
QuerySet methods
create(**kwargs): -> Model
get(*args, **kwargs): -> Model
get_or_none(*args, **kwargs): -> Optional[Model]
get_or_create(_defaults: Optional[Dict[str, Any]] = None, *args, **kwargs) -> Tuple[Model, bool]
first(*args, **kwargs): -> Model
update(each: bool = False, **kwargs) -> int
update_or_create(**kwargs) -> Model
bulk_create(objects: List[Model]) -> None
bulk_update(objects: List[Model], columns: List[str] = None) -> None
delete(*args, each: bool = False, **kwargs) -> int
all(*args, **kwargs) -> List[Optional[Model]]
iterate(*args, **kwargs) -> AsyncGenerator[Model]
filter(*args, **kwargs) -> QuerySet
exclude(*args, **kwargs) -> QuerySet
select_related(related: Union[List, str]) -> QuerySet
prefetch_related(related: Union[List, str]) -> QuerySet
limit(limit_count: int) -> QuerySet
offset(offset: int) -> QuerySet
count(distinct: bool = True) -> int
exists() -> bool
max(columns: List[str]) -> Any
min(columns: List[str]) -> Any
avg(columns: List[str]) -> Any
sum(columns: List[str]) -> Any
fields(columns: Union[List, str, set, dict]) -> QuerySet
exclude_fields(columns: Union[List, str, set, dict]) -> QuerySet
order_by(columns:Union[List, str]) -> QuerySet
values(fields: Union[List, str, Set, Dict])
values_list(fields: Union[List, str, Set, Dict])
Relation types
- One to many - with
ForeignKey(to: Model)
- Many to many - with
ManyToMany(to: Model, Optional[through]: Model)
Model fields types
Available Model Fields (with required args - optional ones in docs):
String(max_length)
Text()
Boolean()
Integer()
Float()
Date()
Time()
DateTime()
JSON()
BigInteger()
SmallInteger()
Decimal(scale, precision)
UUID()
LargeBinary(max_length)
Enum(enum_class)
Enum like Field - by passing choices to any other Field type
EncryptedString - by passing encrypt_secret and encrypt_backend
ForeignKey(to)
ManyToMany(to)
Available fields options
The following keyword arguments are supported on all field types.
primary_key: bool
nullable: bool
default: Any
server_default: Any
index: bool
unique: bool
choices: typing.Sequence
name: str
All fields are required unless one of the following is set:
nullable - Creates a nullable column. Sets the default to False. Read the fields common parameters for details.
sql_nullable - Used to set different setting for pydantic and the database. Sets the default to nullable value. Read the fields common parameters for details.
default - Set a default value for the field. Not available for relation fields
server_default - Set a default value for the field on server side (like sqlalchemy's func.now()). Not available for relation fields
primary key with autoincrement - When a column is set to primary key and autoincrement is set on this column.
Autoincrement is set by default on int primary keys.
Available signals
Signals allow to trigger your function for a given event on a given Model.
pre_save
post_save
pre_update
post_update
pre_delete
post_delete
pre_relation_add
post_relation_add
pre_relation_remove
post_relation_remove
post_bulk_update