ORMX (Python)
Pydantic-native async ORM for Postgres, with a Rust core for the hot path and a real migration engine.
The headline trick: one class plays three roles — DB persistence object, framework request body, OpenAPI response model — without a translation layer. Drop into Rivex, FastAPI, Litestar, anything that knows Pydantic.
from rivex import Depends, Rivex
from ormx import Field, Model, TransactionMiddleware, connect, disconnect, get_db
class User(Model):
__tablename__ = "users"
id: int = Field(primary_key=True)
email: str = Field(unique=True, db_index=True, pattern=r"^[^@\s]+@[^@\s]+$")
name: str = Field(min_length=1, max_length=100)
age: int = Field(ge=0, le=120)
app = Rivex()
app.add_middleware(TransactionMiddleware)
@app.on_event("startup")
async def init(): await connect("postgres://localhost/myapp")
@app.on_event("shutdown")
async def close(): await disconnect()
@app.get("/users/{id}", response_model=User)
async def get_user(id: int, db=Depends(get_db)):
return await User.get(id)
@app.post("/users", response_model=User)
async def create_user(user: User, db=Depends(get_db)):
return await user.save()
That's the entire stack.
Status
4.4.2 — stable. 345 tests passing.
| Feature | Status |
|---|---|
Pydantic-native Model (every constraint, alias, validator) |
✅ |
Framework-agnostic TransactionMiddleware |
✅ |
Async-generator get_db dependency |
✅ |
Rust transaction API (Engine.begin() / tx.commit() / tx.rollback()) |
✅ |
| Multi-statement DDL via simple-query protocol | ✅ |
One-to-many eager loading via .include() |
✅ |
Migration engine — CREATE/DROP TABLE, ADD/DROP COLUMN, ALTER COLUMN TYPE, indexes, foreign keys with ON DELETE/ON UPDATE, multi-schema, NULL ↔ NOT NULL |
✅ |
| Sharding — hash / range / geo / list / custom routing | ✅ |
Multi-schema (__schema__ on Model) |
✅ |
Query builder — where / order_by / limit / offset / select / distinct / group_by / having / count / exists / scalar / values / update / delete / stream |
✅ |
Streaming reads via server-side cursor (.stream(chunk_size=)) |
✅ |
Model.refresh() for re-reading rows touched externally |
✅ |
| BYTEA / bytes round-trip | ✅ (since v3.0.2) |
| Many-to-many eager loading | 🚧 |
| Beyond Postgres (MySQL / SQLite drivers) | 🚧 |
| Composite (multi-column) indexes + foreign keys | 🚧 |
Install
pip install ormx-py
The PyPI distribution is
ormx-py— theormxandpyormxnames there belong to unrelated packages. The import isormx.
The Rust core is compiled into the wheel (vendored at
vendor/ormx-core, tracking
core.ormx.vexarr.com),
with abi3 wheels for Linux (x86_64 + aarch64), macOS (Intel + Apple
Silicon), and Windows — one pip install, no separate core package.
Source builds need rustc 1.75+ and a submodule-initialized checkout
(git clone --recurse-submodules). See RELEASING.md
for the full distribution story.
Querying — the cookbook
ORMX query builders are chainable. Every chain method returns self;
terminal methods (all, first, count, exists, scalar,
values, update, delete, stream) execute the query and return.
# Basic WHERE
users = await User.query().where(User.age > 18).all()
# Chained — AND
adults = await (
User.query()
.where(User.age > 18)
.where(User.email.like("%@example.com"))
.order_by("name")
.limit(50)
.all()
)
# Count / exists / scalar
total = await User.query().count()
has_admin = await User.query().where(User.email == "admin@x.com").exists()
# Aggregates with GROUP BY + HAVING (HAVING is a raw SQL fragment)
buckets = await (
User.query()
.select("age")
.group_by("age")
.having("count(*) > 5")
.all(raw=True)
)
# → [{"age": 30}, {"age": 31}, ...]
# DISTINCT and DISTINCT ON
unique_ages = await User.query().select("age").distinct().values("age")
latest_per_user = await (
Event.query()
.order_by("user_id").order_by("created_at", "desc")
.distinct("user_id")
.all()
)
# Bulk update / delete (skip per-row hooks)
n = await User.query().where(User.age < 13).update(age=13)
n = await User.query().where(User.deleted_at.is_not_null()).delete()
# Streaming — memory-bounded, server-side cursor
async for user in User.query().where(User.age > 50).stream(chunk_size=1000):
process(user)
# Refresh a stale instance after an external write
await user.refresh() # re-reads by PK
Relationships and eager loading
class User(Model):
__tablename__ = "users"
id: int = Field(primary_key=True)
posts = Relationship("Post", back_populates="user")
class Post(Model):
__tablename__ = "posts"
id: int = Field(primary_key=True)
user_id: int = ForeignKey("users.id", on_delete="CASCADE")
title: str = Field(db_index=True)
user = Relationship("User", back_populates="posts")
# One IN-query per included relation — no N+1.
users = await User.query().include("posts").all()
for u in users:
print(u.name, [p.title for p in u.posts])
Transactions
Three flavours, pick what fits:
# 1. Explicit context manager
async with ormx.transaction():
await User.create(name="alice")
await Order.create(user_id=1, total=10)
# Rollback automatic on exception; commit on clean exit.
# 2. Unit-of-Work (batched flush at the end)
async with ormx.uow_session():
user = User(id=1, name="alice", age=30)
user.age = 31 # implicitly dirty
await user.save() # flushed at context exit, all in one tx
# 3. Per-request middleware (the most common shape)
app.add_middleware(TransactionMiddleware)
# Every handler wrapped automatically.
# Default: rollback on 5xx responses + raised exceptions; configurable.
Migrations
# Diff models vs DB, write a new timestamped migration file
ormx makemigrations
# Apply unapplied migrations in order
ormx migrate
# Roll back the most recently applied one
ormx migrate --rollback
# Drop orphan tables (off by default — safety against accidental data loss)
ormx makemigrations --allow-drop-tables
State lives in an ormx_migrations table the runner creates on first
use. Each migration runs in its own transaction; partial-failure
rolls back cleanly.
Detects: CREATE/DROP TABLE, ADD/DROP COLUMN, ALTER COLUMN TYPE (with
USING), CREATE/DROP INDEX, ADD/DROP FOREIGN KEY, SET/DROP NOT NULL,
CREATE SCHEMA for non-public-schema models. Multi-statement DDL +
PL/pgSQL with dollar-quoted bodies work via the Postgres simple-query
protocol — executor.execute_many(sql).
Multi-schema
class AuditEvent(Model):
__tablename__ = "events"
__schema__ = "audit" # ← non-public schema; CREATE SCHEMA emitted automatically
id: int = Field(primary_key=True)
event_type: str
# Cross-schema FK
class AuditRef(Model):
__tablename__ = "refs"
event_id: int = ForeignKey("audit.events.id")
Sharding
import ormx_core
from ormx import set_router
from ormx.sharding import Router, HashSharding
# Connect each shard
await ormx.connect("postgres://shard0/...", shard_name="shard0", is_default=True)
await ormx.connect("postgres://shard1/...", shard_name="shard1")
# Install a routing strategy
set_router(Router(shards=["shard0", "shard1"], strategy=HashSharding()))
# Pass ``sharding_value`` and ORMX routes to the right shard
await User.query().where(User.id == 42).first(sharding_value=42)
Hash / range / geo / list / custom routing all supported via
ormx_core.ShardingConfig.
Settings management
ORMX itself doesn't ship one — use pydantic-settings (standard) or
write a small helper. Combined with Rivex's
Settings base class:
from rivex import Settings
class Config(Settings):
database_url: str
debug: bool = False
cfg = Config.from_env()
await ormx.connect(cfg.database_url)
Upgrading from 2.x
See MIGRATION-3.0.md. TL;DR: type annotations on
fields are now required (Pydantic uses them); ormx.fastapi was
replaced by framework-agnostic primitives (TransactionMiddleware +
get_db work with FastAPI, Rivex, Litestar — anything that knows
async-generator dependencies).
Examples
examples/rivex_basic.py— full CRUD with Rivex (one file)examples/fastapi_basic.py— same model, FastAPIexamples/blog/— full app: User + Post + Comment with relationships, eager loading, transactions, curl walkthrough
Documentation
Full docs at docs/ — mkdocs serve to read locally,
auto-publishes to https://ormx.dev/docs via the docs workflow.
License
MIT.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distributions
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 ormx_py-4.4.3-cp37-abi3-macosx_11_0_arm64.whl.
File metadata
- Download URL: ormx_py-4.4.3-cp37-abi3-macosx_11_0_arm64.whl
- Upload date:
- Size: 4.5 MB
- Tags: CPython 3.7+, macOS 11.0+ ARM64
- Uploaded using Trusted Publishing? No
- Uploaded via:
twine/6.2.0 CPython/3.13.7
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2df727ed66cfb4604d7fa92ee860b6f39b282125fcfaa4237dc403239eb28218
|
|
| MD5 |
d8d76ebf170715dabe677a98ff217270
|
|
| BLAKE2b-256 |
707ce2be453b794a7f10466baa5393da57980f89999e537c3f698cc09704aca6
|