Async ORM for Pydantic models and PostgreSQL, with a Django-inspired API
Project description
AirModel
Async ORM for Pydantic models and PostgreSQL, with a Django-inspired query API.
- GitHub | PyPI | Documentation
- Created by Audrey M. Roy Greenfeld | GitHub @audreyfeldroy | PyPI @audreyr
- MIT License
Define your models with standard Pydantic type annotations. AirModel turns them into PostgreSQL tables and gives you async create, get, filter, all, count, save, and delete, plus Django-style lookups like price__gte=10 and name__icontains="dragon".
from airmodel import AirDB, AirModel, AirField
class UnicornSighting(AirModel):
id: int | None = AirField(default=None, primary_key=True)
location: str
sparkle_rating: int
confirmed: bool = AirField(default=False)
# In your async handlers:
await UnicornSighting.create(location="Rainbow Falls", sparkle_rating=11)
sighting = await UnicornSighting.get(id=1)
bright_ones = await UnicornSighting.filter(sparkle_rating__gte=8, confirmed=True)
count = await UnicornSighting.count()
AirField() works like Pydantic's Field() but adds primary_key=True and UI presentation metadata (label, widget, placeholder, etc.).
Built on asyncpg and Pydantic v2. Works with Air or any async Python project.
Install
uv add AirModel
Connect to PostgreSQL
With Air
Zero config. Set DATABASE_URL in the environment and Air connects automatically:
import air
from airmodel import AirModel, AirField
app = air.Air() # reads DATABASE_URL, connects on startup
class Item(AirModel):
id: int | None = AirField(default=None, primary_key=True)
name: str
If DATABASE_URL is not set, app.db is None and no database is configured. The pool is available as app.db for transactions and table creation.
With any async Python project
import asyncpg
from airmodel import AirDB
db = AirDB()
pool = await asyncpg.create_pool("postgresql://user:pass@host/dbname")
db.connect(pool)
# ... use your models ...
await pool.close()
db.disconnect()
Creating tables
Call create_tables() after the pool is ready:
await db.create_tables()
This runs CREATE TABLE IF NOT EXISTS for every AirModel subclass and auto-migrates existing tables: any model fields not yet in the database get added via ALTER TABLE ADD COLUMN. Non-destructive: never drops columns, never changes types. New columns are added without NOT NULL so existing rows aren't broken; Pydantic still enforces requirements at the app layer.
Query API
Every method is async. Table names are derived from the module and class name, so models in different projects sharing one database won't collide. A UnicornSighting defined in myapp/models.py becomes myapp_unicorn_sighting. For standalone files with generic names like main.py, the prefix comes from pyproject.toml's project name.
CRUD
# Create
sighting = await UnicornSighting.create(location="Rainbow Falls", sparkle_rating=11)
# Read one (returns None if not found, raises MultipleObjectsReturned if ambiguous)
sighting = await UnicornSighting.get(id=1)
# Read many
all_sightings = await UnicornSighting.all()
all_sorted = await UnicornSighting.all(order_by="-sparkle_rating", limit=10)
confirmed = await UnicornSighting.filter(confirmed=True, order_by="-sparkle_rating")
page = await UnicornSighting.filter(confirmed=True, limit=10, offset=20)
# filter() with no filter kwargs is equivalent to all():
everything = await UnicornSighting.filter(order_by="location")
# Count
total = await UnicornSighting.count()
bright = await UnicornSighting.count(sparkle_rating__gte=8)
# Update
sighting.sparkle_rating = 12
await sighting.save()
await sighting.save(update_fields=["sparkle_rating"]) # partial update
# Delete
await sighting.delete()
Django-style lookups
Append __lookup to any field name in filter(), get(), or count():
| Lookup | SQL | Example |
|---|---|---|
field__gt |
> |
sparkle_rating__gt=5 |
field__gte |
>= |
sparkle_rating__gte=5 |
field__lt |
< |
sparkle_rating__lt=10 |
field__lte |
<= |
sparkle_rating__lte=10 |
field__contains |
LIKE '%...%' |
location__contains="Falls" |
field__icontains |
ILIKE '%...%' |
location__icontains="falls" |
field__in |
= ANY(...) |
sparkle_rating__in=[8, 9, 10] |
field__isnull |
IS NULL / IS NOT NULL |
confirmed__isnull=True |
Bulk operations
Single-query operations that minimize round trips. Both bulk_update() and bulk_delete() require at least one filter argument to prevent accidental mass operations.
# Insert many rows in one INSERT ... RETURNING *
sightings = await UnicornSighting.bulk_create([
{"location": "Rainbow Falls", "sparkle_rating": 11},
{"location": "Crystal Cave", "sparkle_rating": 8},
])
# UPDATE ... WHERE with row count
updated = await UnicornSighting.bulk_update(
{"confirmed": True}, sparkle_rating__gte=10
)
# DELETE ... WHERE with row count
deleted = await UnicornSighting.bulk_delete(confirmed=False)
Transactions
# With Air: app.db — without Air: your AirDB() instance
async with app.db.transaction():
await UnicornSighting.create(location="Rainbow Falls", sparkle_rating=11)
await UnicornSighting.create(location="Crystal Cave", sparkle_rating=8)
# Both rows commit together, or neither does.
Supported types
| Python | PostgreSQL |
|---|---|
str |
TEXT |
int |
INTEGER |
float |
DOUBLE PRECISION |
bool |
BOOLEAN |
datetime |
TIMESTAMP WITH TIME ZONE |
UUID |
UUID |
Fields with primary_key=True become BIGSERIAL PRIMARY KEY. Optional fields (str | None) are nullable. Required fields without defaults get NOT NULL.
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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file airmodel-0.4.2.tar.gz.
File metadata
- Download URL: airmodel-0.4.2.tar.gz
- Upload date:
- Size: 77.9 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7bdc7c87c995519ff214abd9d29f057a68863f1e6d17a18b847eb334344849cd
|
|
| MD5 |
635afae7979ad2bdecb2d90e1da16068
|
|
| BLAKE2b-256 |
1c3cdcc0fea314be8c62cb5909e7bb2c20bd4a5720938140d80e602c86a9e538
|
Provenance
The following attestation bundles were made for airmodel-0.4.2.tar.gz:
Publisher:
publish.yml on feldroy/AirModel
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
airmodel-0.4.2.tar.gz -
Subject digest:
7bdc7c87c995519ff214abd9d29f057a68863f1e6d17a18b847eb334344849cd - Sigstore transparency entry: 1149378459
- Sigstore integration time:
-
Permalink:
feldroy/AirModel@783fc98a1fc03e4ba176cc721b727a8e44ef9985 -
Branch / Tag:
refs/tags/v0.4.2 - Owner: https://github.com/feldroy
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@783fc98a1fc03e4ba176cc721b727a8e44ef9985 -
Trigger Event:
push
-
Statement type:
File details
Details for the file airmodel-0.4.2-py3-none-any.whl.
File metadata
- Download URL: airmodel-0.4.2-py3-none-any.whl
- Upload date:
- Size: 13.9 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
541ffeac7307e20494d2cd86ef0ff6cd3e90dc15244b2e56c7dfe5e34edd97b4
|
|
| MD5 |
eb1e92add0057daea26bacf707d2c72c
|
|
| BLAKE2b-256 |
25b7b93c7c37dfc549c83102c91468a66ed89931b7f39349086a48d65328f0f4
|
Provenance
The following attestation bundles were made for airmodel-0.4.2-py3-none-any.whl:
Publisher:
publish.yml on feldroy/AirModel
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
airmodel-0.4.2-py3-none-any.whl -
Subject digest:
541ffeac7307e20494d2cd86ef0ff6cd3e90dc15244b2e56c7dfe5e34edd97b4 - Sigstore transparency entry: 1149378522
- Sigstore integration time:
-
Permalink:
feldroy/AirModel@783fc98a1fc03e4ba176cc721b727a8e44ef9985 -
Branch / Tag:
refs/tags/v0.4.2 - Owner: https://github.com/feldroy
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@783fc98a1fc03e4ba176cc721b727a8e44ef9985 -
Trigger Event:
push
-
Statement type: