A lightweight ORM built on Pydantic for simple CRUD operations with minimal code
Project description
Ormantism
A lightweight ORM built on Pydantic for simple CRUD with minimal code. Use it when you need straightforward database access without the overhead of a full-featured ORM.
Supported backends: SQLite (built-in), MySQL, PostgreSQL. Database URLs use the same style as SQLAlchemy.
Features
- Pydantic-based models — Define tables with type hints and optional defaults
- Auto table creation — Tables are created on first use; new columns are added when the model gains fields
- Relationships — Single and list references to other tables; lazy loading by default
- Preloading — Eager-load relations with JOINs to avoid N+1 queries
- Fluent Query API —
Model.q().where(...).select(...).order_by(...).first()/.all() - Timestamps — Optional
created_at/updated_at/deleted_atand soft deletes - Versioning — Optional row versioning so updates can create new rows instead of overwriting
- Load-or-create — Find by criteria or create in one call, with control over which fields are used for the lookup
- Transactions — Context manager with automatic commit/rollback
Installation
pip install ormantism
SQLite works with no extra dependencies. For MySQL or PostgreSQL, install the corresponding extra:
pip install ormantism[mysql] # pymysql
pip install ormantism[postgresql] # psycopg2
pip install ormantism[mysql,postgresql]
Requires: Python 3.12+, Pydantic 2.x.
Quick start
Connect
import ormantism
ormantism.connect("sqlite:///my_app.db")
# or: sqlite://:memory: | mysql://user:pass@host/db | postgresql://user:pass@host/db
Define models
from ormantism import Table
from typing import Optional
class User(Table, with_timestamps=True):
name: str
email: str
age: Optional[int] = None
class Post(Table, with_timestamps=True):
title: str
content: str
author: User | None = None
Create and query
# Create (saved automatically)
user = User(name="Alice", email="alice@example.com", age=30)
post = Post(title="Hello", content="World", author=user)
# Query: one row
user = User.q().where(User.id == 1).first()
user = User.q().where(name="Alice").first()
# Query: all matching rows
posts = Post.q().where(author=user).all()
# Update
user.age = 31 # auto-saved on assignment
user.update(age=31, email="alice@new.com")
# Delete (soft delete when with_timestamps=True)
user.delete()
Query API
The primary way to query is Model.q(), which returns a fluent Query builder. Chain methods and end with .first(), .all(), or iterate.
Basic usage
# One row or None
user = User.q().where(User.id == 1).first()
user = User.q().where(name="Alice").first()
# All matching rows
users = User.q().where(age__gte=18).all()
users = list(User.q().where(name="Bob"))
# Limit and offset
users = User.q().limit(10).all()
page = User.q().offset(20).limit(10).all()
Where: expression-style and Django-style
Expression-style — SQLAlchemy-like, using model attributes and operators:
User.q().where(User.name == "Alice").first()
User.q().where(User.age >= 18, User.email.is_not_null()).all()
User.q().where(Post.author.name.icontains("smith")).all() # filter by related column
Django-style kwargs — field__lookup=value:
User.q().where(name="Alice") # exact (default)
User.q().where(name__icontains="alice") # case-insensitive contains
User.q().where(age__gte=18, age__lt=65) # gt, gte, lt, lte
User.q().where(name__in=["Alice", "Bob"]) # IN
User.q().where(name__range=(1, 10)) # BETWEEN
User.q().where(author__isnull=True) # IS NULL
User.q().where(book__title__contains="Python") # nested path
Supported lookups: exact, iexact, lt, lte, gt, gte, in, range, isnull, contains, icontains, startswith, istartswith, endswith, iendswith, like, ilike.
Select and preload
Use select() to choose which columns/relations to fetch. Relations in select() are eager-loaded (JOINs), avoiding N+1 lazy loads.
# Preload a relation (all columns from root + author)
book = Book.q().select("author").where(Book.id == 1).first()
book.author # no lazy load
# Preload nested path
book = Book.q().select("author.publisher").where(Book.id == 1).first()
# Multiple relations
users = User.q().select("profile", "posts").where(User.active == True).all()
# Expression-style
User.q().select(User.name, User.book.title).where(...)
Without select() for a relation, accessing row.author triggers a lazy load (and a warning).
Order, limit, offset
User.q().order_by(User.name).all() # ascending
User.q().order_by(User.created_at.desc).all() # descending
User.q().order_by(User.name, User.id).all() # multiple columns
User.q().limit(10).offset(20).all()
Soft-deleted rows
For tables with with_timestamps=True, soft-deleted rows are excluded by default. Include them with:
User.q().include_deleted().where(User.id == 1).first()
Query execution
| Method | Returns |
|---|---|
.first() |
One Model or None |
.all(limit=N) |
List of Model |
list(q) |
Same as .all() |
for row in q: |
Iterate (lazy) |
Model options
Timestamps and soft delete
class Post(Table, with_timestamps=True):
title: str
content: str
# Adds: created_at, updated_at, deleted_at. delete() becomes soft delete.
Only some timestamps:
class Log(Table, with_created_at_timestamp=True, with_timestamps=False):
message: str
Versioning
When specified fields change on update, a new row is created instead of updating in place:
class Document(Table, versioning_along=("name",)):
name: str
content: str
doc = Document(name="foo", content="v1")
doc = Document(name="foo", content="v2") # New row; same name, new content
Named connection
class Remote(Table, connection_name="secondary"):
...
Field types
- Scalars:
int,float,str,bool,datetime.datetime,enum.Enum - Nullable:
Optional[T] = None - Defaults:
age: int = 0 - JSON:
list,dict, orormantism.JSON(arbitrary JSON in a column) - Relations:
Author(single),Optional[Author],list[Child] - Generic reference:
ref: Table(any table; cannot be preloaded) - Pydantic models: Stored as JSON
Relationships
class Category(Table, with_timestamps=True):
name: str
class Post(Table, with_timestamps=True):
title: str
category: Category | None = None
tags: list[Category] = []
# Self-reference
class Node(Table, with_timestamps=True):
parent: Optional["Node"] = None
name: str
Load or create
Find by given fields or create; other fields update the row if it exists or set values on create:
user = User.load_or_create(_search_fields=("name",), name="Alice", email="alice@example.com")
# Same row, email updated:
user2 = User.load_or_create(_search_fields=("name",), name="Alice", email="new@example.com")
Transactions
from ormantism import transaction
with transaction():
User(name="Alice", email="alice@example.com")
User(name="Bob", email="bob@example.com")
# Commits on exit; rolls back on exception
Use transaction(connection_name="...") when using a named connection.
API summary
Table: create and persist
Model(**kwargs)— Create and save a rowinstance.field = value— Assign and auto-saveinstance.update(**kwargs)— Update fields and saveinstance.delete()— Delete (soft if timestamps enabled)
Table: query builder
Model.q()— Return aQueryfor this table (supports_transform_queryfrom mixins)
Query: fluent chain
q.where(*exprs, **kwargs)— Filter (expressions and/or Django-style kwargs)q.filter(...)— Alias forwhereq.select(*paths)— Preload relations (e.g."author","author.publisher")q.order_by(*exprs)— ORDER BY (e.g.User.name,User.created_at.desc)q.limit(n)/q.offset(n)— Paginationq.include_deleted()— Include soft-deleted rowsq.first()— One row or Noneq.all(limit=N)— List of rowsq.update(**kwargs)— Update matched rowsq.delete()— Delete matched rows
Table: load or create
Model.load_or_create(_search_fields=(...), **data)— Load by search fields or create; other fields update or populate
Connection and transaction
ormantism.connect(url)— Set default connection (SQLAlchemy-style URL)ormantism.transaction(connection_name=...)— Context manager for transactions
Table class options
| Option | Effect |
|---|---|
with_timestamps=True |
Add created_at, updated_at, deleted_at; soft delete |
with_created_at_timestamp=True |
Only created_at |
with_updated_at_timestamp=True |
Only updated_at |
versioning_along=("field",) |
New row when these fields change on update |
connection_name="name" |
Use named connection (inherited by subclasses) |
Deprecated: load and load_all
Model.load(**criteria) and Model.load_all(**criteria) are deprecated. Use the Query API instead:
# Instead of: User.load(id=1)
User.q().where(id=1).first()
# Instead of: User.load_all(name="Alice")
User.q().where(name="Alice").all()
# Instead of: Book.load(id=1, preload="author")
Book.q().select("author").where(Book.id == 1).first()
# Instead of: User.load_all(with_deleted=True)
User.q().include_deleted().all()
Code reference
For a full code reference (classes and methods with descriptions, file/line, and usages), see ormantism/REFERENCE.md.
Limitations
- Migrations — New columns are added automatically; dropping/renaming columns or changing types is not automated (see TODO.md).
- Relations — Single and list references; no built-in many-to-many tables.
- Generic references —
ref: Tablecannot be preloaded (JOIN not supported).
License and contributing
License: MIT.
Contributions are welcome. See TODO.md for ideas and planned improvements.
Project details
Release history Release notifications | RSS feed
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 ormantism-0.7.0.tar.gz.
File metadata
- Download URL: ormantism-0.7.0.tar.gz
- Upload date:
- Size: 44.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a49024d4200b861bae8c37b2c490d659a06220534e9901e8def9d83795ef628f
|
|
| MD5 |
d40ee16a209b5cddea0e28e174cbf286
|
|
| BLAKE2b-256 |
834e35f7d19008f37162a7a5e59a5fe0c315cb3917c28cb1bcbb3c12bb4cd5cd
|
File details
Details for the file ormantism-0.7.0-py3-none-any.whl.
File metadata
- Download URL: ormantism-0.7.0-py3-none-any.whl
- Upload date:
- Size: 54.4 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
b8a477898e455148b24d385dda014a24f4ad239f1bb17e4c2c37db8ca16be504
|
|
| MD5 |
fa16a8a779be8a06ff403c6e8bdba08d
|
|
| BLAKE2b-256 |
e63ba0eeae4f5dcc9bfa2292df5c0deca0f098e4de275f4e93d01730263bc544
|