Skip to main content

Async ORM for Python — fully async from connection to migration

Project description

async-orm

An async fork of Masonite ORM — the same familiar API, fully async from connection to migration.

Fork notice: This project is based on masonite-orm v2.24.0 by Joe Mancuso, licensed under MIT. All connection, query, model, relationship, scope, schema, and migration layers have been converted to use async/await.

What changed from Masonite ORM

Layer Original This fork
Connections Synchronous drivers aiosqlite, aiomysql, asyncpg, aioodbc
QueryBuilder .get(), .first(), .count() are sync Terminal methods return coroutines (await)
Models User.all() returns a collection await User.all() returns a collection
Relationships Sync eager loading await User.with_("posts").find(1)
Schema/Blueprint with schema.create(...) as table: async with schema.create(...) as table:
Migrations def up(self): async def up(self):
CLI commands Direct calls asyncio.run() bridge

Chain methods (where, order_by, select, limit, etc.) remain synchronous — only terminal methods that hit the database are async.

Installation

pip install masonite-orm-async

With optional Pydantic support:

pip install masonite-orm-async[pydantic]

Quick start

Models

from masoniteorm.models import Model
from masoniteorm.relationships import has_many

class User(Model):
    __connection__ = "sqlite"

    id: int
    name: str
    email: str

    @has_many("id", "user_id")
    def posts(self):
        return Post

class Post(Model):
    __connection__ = "sqlite"

    id: int
    user_id: int
    title: str
    body: str

Field annotations are optional but give you IDE autocomplete on user.name, user.email, etc. They are bare annotations with no default values — they don't affect runtime behavior.

Queries

# All terminal operations are awaited
users = await User.all()
user = await User.find(1)
user = await User.where("email", "alice@example.com").first()

# Chain methods are still synchronous
await User.where("active", True).order_by("name").limit(10).get()

# CRUD
user = await User.create({"name": "Alice", "email": "alice@example.com"})
user.name = "Alice Updated"
await user.save()
await user.delete()

# Eager loading
user = await User.with_("posts").find(1)

Migrations

from masoniteorm.migrations import Migration

class CreateUsersTable(Migration):
    async def up(self):
        async with self.schema.create("users") as table:
            table.increments("id")
            table.string("name")
            table.string("email").unique()
            table.timestamps()

    async def down(self):
        await self.schema.drop("users")

Running migrations programmatically

import asyncio
from masoniteorm.migrations import Migration

async def main():
    migration = Migration(
        connection="sqlite",
        migration_directory="databases/migrations",
        config_path="config",
    )
    await migration.create_table_if_not_exists()
    await migration.migrate()

asyncio.run(main())

FastAPI integration

from contextlib import asynccontextmanager
from fastapi import FastAPI
from masoniteorm.migrations import Migration

@asynccontextmanager
async def lifespan(app: FastAPI):
    migration = Migration(
        connection="sqlite",
        migration_directory="databases/migrations",
        config_path="config",
    )
    await migration.create_table_if_not_exists()
    await migration.migrate()
    yield
    from config import DB
    await DB.close_all()

app = FastAPI(lifespan=lifespan)

@app.get("/users")
async def list_users():
    users = await User.all()
    return [u.model_dump() for u in users]

Sanic integration

from sanic import Sanic, json
from masoniteorm.migrations import Migration

app = Sanic("MyApp")

@app.before_server_start
async def setup(app, loop):
    migration = Migration(
        connection="sqlite",
        migration_directory="databases/migrations",
        config_path="config",
    )
    await migration.create_table_if_not_exists()
    await migration.migrate()

@app.after_server_stop
async def teardown(app, loop):
    from config import DB
    await DB.close_all()

@app.get("/users")
async def list_users(request):
    users = await User.all()
    return json([u.serialize() for u in users])

Type hints

All generic classes are subscriptable at runtime and have .pyi stubs for full IDE/type-checker support:

from masoniteorm.collection import Collection
from masoniteorm.query import QueryBuilder

# IDE knows: users is Collection[User], user is User | None
users = await User.all()       # → Collection[User]
user = await User.find(1)      # → User | None

# Chain methods preserve the type
query = User.where("active", True).order_by("name").limit(10)  # → QueryBuilder[User]
results = await query.get()    # → Collection[User]

Pydantic integration (optional)

Auto-generate Pydantic schemas from your models:

# Get the Pydantic model class
UserSchema = User.to_schema()  # → UserSchema(id: int | None, name: str | None, email: str | None)

# Serialize a model instance through Pydantic
user = await User.find(1)
data = user.model_dump()       # → {"id": 1, "name": "Alice", "email": "alice@example.com"}

# Use the schema directly for validation
validated = UserSchema(name="Bob", email="bob@example.com")

If pydantic is not installed, model_dump() falls back to serialize().

Configuration

Create a config.py (or config/database.py) that exposes a DB object:

from masoniteorm.connections import ConnectionResolver

DATABASES = {
    "default": "sqlite",
    "sqlite": {
        "driver": "sqlite",
        "database": "app.db",
    },
}

DB = ConnectionResolver().set_connection_details(DATABASES)

Set the config path via environment variable or pass it directly:

import os
os.environ["DB_CONFIG_PATH"] = "config"

Supported databases

Database Async driver Connection key
SQLite aiosqlite sqlite
MySQL aiomysql mysql
PostgreSQL asyncpg postgres
MSSQL aioodbc mssql

Transactions

from config import DB

# Context manager — auto-rollback on exception
async with DB.transaction():
    user = await User.create({"name": "Alice", "email": "alice@example.com"})
    await Post.create({"title": "Hello", "user_id": user.id})

# Manual control
await DB.begin_transaction()
await User.create({"name": "Bob", "email": "bob@example.com"})
await DB.commit()  # or DB.rollback()

Connection resilience

Pool creation retries automatically with exponential backoff (configurable):

DATABASES = {
    "default": "postgres",
    "postgres": {
        "driver": "postgres",
        "host": "127.0.0.1",
        "database": "mydb",
        "user": "myuser",
        "password": "mypass",
        "port": 5432,
        "connection_retries": 3,           # retry pool creation (default: 3)
        "connection_pool_timeout": 30,     # seconds to wait for a connection (default: 30)
        "connection_pooling_min_size": 1,
        "connection_pooling_max_size": 10,
    },
}

When the pool is exhausted, a PoolExhaustedError is raised instead of hanging:

from masoniteorm.exceptions import PoolExhaustedError

Testing

# ORM unit tests (SQLite, no dependencies)
python orm/test_async.py

# FastAPI demo tests
cd fastapi_demo && PYTHONPATH="../orm/src:." python -m pytest test_app.py -v

# Integration tests (requires Docker)
docker compose up -d --wait
PYTHONPATH="orm/src:." python -m pytest tests/ -v

# Load test
PYTHONPATH="orm/src:." python tests/load_test.py --db postgres --ops 1000 --concurrency 50

License

MIT — same as the original Masonite ORM. See LICENSE.

Credits

  • Masonite ORM by Joe Mancuso and contributors — the original synchronous ORM this project is forked from.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

masonite_orm_async-1.2.0.tar.gz (89.0 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

masonite_orm_async-1.2.0-py3-none-any.whl (138.4 kB view details)

Uploaded Python 3

File details

Details for the file masonite_orm_async-1.2.0.tar.gz.

File metadata

  • Download URL: masonite_orm_async-1.2.0.tar.gz
  • Upload date:
  • Size: 89.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.12.9

File hashes

Hashes for masonite_orm_async-1.2.0.tar.gz
Algorithm Hash digest
SHA256 9c33c45c064b09d67b1ae761e6692f89276756c1f4451dd2ba8c5cd32b38303c
MD5 5c2e8fe3c4fc53864b47b7d87d818839
BLAKE2b-256 5c546c073bfa107322868ebacda8cd9c105a6ace786f72e439536fcc544811f5

See more details on using hashes here.

File details

Details for the file masonite_orm_async-1.2.0-py3-none-any.whl.

File metadata

File hashes

Hashes for masonite_orm_async-1.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 1646a9cfc53720b141b6e457164b80c400374adee0754811cb7b4abe611335ed
MD5 0ad7d891b82957ca997e96cff9619dd5
BLAKE2b-256 d8be81224149c5a65c525814c12cc9ac2f858a0ac49449b8d9ecfd58320edce6

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page