Async-native ORM for PostgreSQL, SQLite, and MySQL/MariaDB
Project description
KakaORM
An async-native ORM for Python. Supports PostgreSQL (asyncpg / psycopg3), SQLite (aiosqlite), and MySQL/MariaDB (aiomysql) as backends, providing Django ORM-like model definitions and type-safe query building.
Features
- Fully async —
async/await-based API that integrates naturally withasyncio - Type-safe queries — Build queries without strings using operator overloading:
User.age >= 20 - Multi-database — Supports PostgreSQL (asyncpg / psycopg3), SQLite (aiosqlite), and MySQL/MariaDB (aiomysql)
- Auto migrations — Detects diff between models and DB schema and generates
ALTER TABLE - Generic descriptors — Type annotation inference via
Column[T]for correct IDE completion - Event hooks — Define
before_insert/after_updateetc. directly on your Model - Relation definitions — Declare FK navigation (forward and reverse) with
has_many()/has_one()/belongs_to() - Pydantic v2 integration — Implements
__get_pydantic_core_schema__/__get_pydantic_json_schema__; use KakaORM models directly as FastAPIresponse_model - Eager loading — Batch-fetch related models with
prefetch()to eliminate N+1 queries - Migration autogenerate — Auto-generate diff files with
autogenerate(); manage withrun_files()+downgrade() - CTE (WITH clause) — Structure complex queries with
with_cte(name, queryset) - Deletion strategies —
SoftDeleteModel(logical deletion) andArchiveModel(archive deletion) base classes; switchdelete()behavior simply by changing inheritance - Validation — Attach validators to columns (
min_length,max_length,min_value,max_value,regex,one_of);save()auto-validates and raisesValidationErrorbefore any DB write - Upsert —
get_or_create()andupdate_or_create()return(instance, created: bool) - Bulk update —
bulk_update(instances, fields=[...])flushes many instances in a singleexecutemanycall
Installation
# SQLite (development / testing)
pip install "kakaorm[aiosqlite]"
# PostgreSQL (asyncpg)
pip install "kakaorm[asyncpg]"
# PostgreSQL (psycopg3)
pip install "kakaorm[psycopg3]"
# MySQL / MariaDB
pip install "kakaorm[aiomysql]"
# All drivers
pip install "kakaorm[all]"
Quickstart
import asyncio
import kakaorm
from kakaorm import Model, IntColumn, StrColumn, BoolColumn
class Task(Model):
title = StrColumn(nullable=False)
done = BoolColumn(nullable=False, default=False)
class Meta:
table_name = "task"
async def main():
engine = await kakaorm.connect("sqlite+aiosqlite:///:memory:")
await engine.create_table(Task)
task = await Task.create(title="Try KakaORM")
print(task.id, task.title, task.done) # 1 Try KakaORM False
task.done = True
await task.save()
tasks = await Task.where(Task.done == True)
print(tasks) # [<Task id=1>]
await engine.disconnect()
asyncio.run(main())
Model Definition
from kakaorm import Model, IntColumn, StrColumn, FloatColumn, BoolColumn, DateTimeColumn, ForeignKey
class Author(Model):
name = StrColumn(nullable=False)
email = StrColumn(unique=True, nullable=False)
bio = StrColumn(nullable=True)
class Meta:
table_name = "author"
class Post(Model):
title = StrColumn(nullable=False)
body = StrColumn(nullable=True)
published = BoolColumn(nullable=False, default=False)
views = IntColumn(nullable=False, default=0)
author_id = ForeignKey(Author, nullable=True)
class Meta:
table_name = "post"
An id column is added automatically as the primary key.
Custom Primary Keys
Set primary_key=True on any column to make it the primary key. No auto-increment is applied.
class Country(Model):
code = StrColumn(primary_key=True, nullable=False) # e.g. "JP" / "US"
name = StrColumn(nullable=False)
class Meta:
table_name = "country"
# Explicit primary key on INSERT
jp = await Country.create(code="JP", name="Japan")
jp.name = "Japan (updated)"
await jp.save() # UPDATE WHERE code = 'JP'
Composite Indexes
Declare indexes in Meta.indexes as a list of tuples. CREATE INDEX is issued automatically when create_table() runs.
class Product(Model):
name = StrColumn(nullable=False)
category = StrColumn(nullable=False)
price = IntColumn(nullable=False)
class Meta:
table_name = "product"
indexes = [
("category", "price"), # composite index
("name",), # single-column index
]
Column Types
| Class | Python type | SQL type |
|---|---|---|
IntColumn |
int |
INTEGER |
StrColumn |
str |
TEXT / VARCHAR(n) |
FloatColumn |
float |
DOUBLE PRECISION |
BoolColumn |
bool |
BOOLEAN |
DateTimeColumn |
datetime |
TIMESTAMP WITH TIME ZONE |
DateColumn |
date |
DATE |
TimeColumn |
time |
TIME |
DecimalColumn |
Decimal |
NUMERIC(p, s) |
ForeignKey |
int |
INTEGER REFERENCES ... |
For column options (nullable, default, unique, primary_key, index, check, auto_increment, auto_now, on_delete, etc.) see the full reference below.
→ Full API reference: docs/REFERENCE.md
CRUD
Create
author = await Author.create(name="Alice", email="alice@example.com")
print(author.id) # DB-generated ID is set
Read
# All records
authors = await Author.all()
# Single record (raises NotFound if not found)
author = await Author.get(Author.email == "alice@example.com")
# Single record (returns None if not found)
author = await Author.get_or_none(Author.id == 1)
# First / last
first = await Author.first()
last = await Author.last()
# As a dict
author = await Author.get(Author.id == 1)
data = author.to_dict() # {"id": 1, "name": "Alice", "email": "..."}
Update
author.name = "Alicia"
await author.save()
Delete
await author.delete()
Validation
from kakaorm.validators import min_length, max_length, min_value, regex
class User(Model):
name = StrColumn(nullable=False, validators=[min_length(2), max_length(50)])
age = IntColumn(nullable=True, validators=[min_value(0)])
email = StrColumn(nullable=False, validators=[
regex(r"^[^@]+@[^@]+\.[^@]+$", message="Enter a valid email address.")
])
class Meta:
table_name = "user"
try:
await User.create(name="A", age=-1, email="bad")
except ValidationError as e:
print(e.errors)
# {"name": ["..."], "age": ["..."], "email": ["..."]}
Upsert
# get_or_create — find or create; returns (instance, created: bool)
author, created = await Author.get_or_create(
email="alice@example.com",
defaults={"name": "Alice"},
)
# update_or_create — find and update, or create
post, created = await Post.update_or_create(
slug="hello-world",
defaults={"title": "Hello World", "published": True},
)
Bulk Operations
# Bulk INSERT (batched into minimal SQL statements)
posts = [Post(title=f"Post {i}", views=0) for i in range(1000)]
await Post.bulk_create(posts)
# Bulk UPDATE — flush many instances at once (uses executemany)
for post in posts:
post.views = 0
await Post.bulk_update(posts, fields=["views"])
# QuerySet-level bulk UPDATE / DELETE
await Post.where(Post.published == False).update(published=True)
await Post.where(Post.views == 0).delete()
# TRUNCATE (also resets sequences)
await Post.truncate()
Database Connections
# SQLite (development / testing)
pip install fastapi uvicorn kakaorm aiosqlite
# PostgreSQL (asyncpg)
pip install fastapi uvicorn kakaorm asyncpg
# PostgreSQL (psycopg3)
pip install fastapi uvicorn kakaorm "psycopg[binary]" psycopg-pool
# MySQL / MariaDB
pip install fastapi uvicorn kakaorm aiomysql
| DB | URL format |
|---|---|
| SQLite (file) | sqlite+aiosqlite:///./app.db |
| SQLite (in-memory) | sqlite+aiosqlite:///:memory: |
| PostgreSQL (asyncpg) | postgresql+asyncpg://user:password@localhost/dbname |
| PostgreSQL (psycopg3) | postgresql+psycopg3://user:password@localhost/dbname |
| MySQL / MariaDB | mysql+aiomysql://user:password@localhost:3306/dbname |
# SQLite (development / testing)
engine = await kakaorm.connect("sqlite+aiosqlite:///:memory:")
engine = await kakaorm.connect("sqlite+aiosqlite:///./dev.db")
# PostgreSQL (asyncpg)
engine = await kakaorm.connect("postgresql+asyncpg://user:password@localhost/dbname")
# PostgreSQL (psycopg3)
engine = await kakaorm.connect("postgresql+psycopg3://user:password@localhost/dbname")
# MySQL / MariaDB (aiomysql)
engine = await kakaorm.connect("mysql+aiomysql://user:password@localhost:3306/dbname")
# Also usable as a context manager
async with await kakaorm.connect("sqlite+aiosqlite:///:memory:") as engine:
...
Framework Integration
- FastAPI — See FastAPI Integration Guide / 日本語
- Flask — See Flask Integration Guide / 日本語
Project Structure
kakaorm/
├── .github/
│ └── workflows/
│ └── ci.yml # GitHub Actions CI (lint + test matrix + MySQL + build)
├── docs/
│ ├── FASTAPI.md # FastAPI integration guide (English)
│ ├── FASTAPI.ja.md # FastAPI integration guide (Japanese)
│ ├── FLASK.md # Flask integration guide (English)
│ ├── FLASK.ja.md # Flask integration guide (Japanese)
│ ├── REFERENCE.md # Full API reference (English)
│ └── REFERENCE.ja.md # Full API reference (Japanese)
├── kakaorm/ # Package source
│ ├── __init__.py # Public API re-exports
│ ├── py.typed # PEP 561 type marker
│ ├── engine.py # Engine base class + AsyncpgEngine / AioSQLiteEngine / AioMySQLEngine / Psycopg3Engine, connect()
│ ├── model.py # Model base class, AsyncORMMeta metaclass
│ ├── query.py # QuerySet (lazy query builder)
│ ├── validators.py # ValidationError + built-in validators (min_length, max_length, …)
│ ├── soft_delete.py # SoftDeleteModel / SoftDeleteQuerySet (logical deletion)
│ ├── archive.py # ArchiveModel / ArchiveQuerySet (archive deletion)
│ ├── relationship.py # has_many / has_one / belongs_to descriptors
│ ├── columns/
│ │ ├── base.py # Column[T] base class, ColumnMeta, WhereClause
│ │ └── types.py # IntColumn, StrColumn, FloatColumn, BoolColumn,
│ │ # DateTimeColumn, DateColumn, TimeColumn, DecimalColumn, ForeignKey
│ └── migration/
│ └── __init__.py # Migrator, VersionedMigrator, MigrationPlan
├── examples/
│ ├── blog_example.py # Blog system usage example
│ ├── fastapi_todo.py # FastAPI TODO list API
│ └── flask_todo.py # Flask TODO list API
├── tests/
│ ├── conftest.py
│ ├── test_crud.py
│ ├── test_joins.py
│ ├── test_aggregates.py
│ ├── test_transaction.py
│ ├── test_bulk_create.py
│ ├── test_bulk_update.py # bulk_update()
│ ├── test_validation.py # Column validators + ValidationError
│ ├── test_upsert.py # get_or_create / update_or_create
│ ├── test_raw_sql.py
│ ├── test_migration.py
│ ├── test_indexes.py # Composite indexes
│ ├── test_custom_pk.py # Custom primary keys
│ ├── test_hooks.py # Event hooks
│ ├── test_relationship.py # Relation definitions
│ └── test_security.py # Security regression tests
├── CHANGELOG.md # Version history
├── LICENSE # MIT License
├── pyproject.toml # Package metadata and build configuration
└── ruff.toml # Ruff configuration
Running Tests
pip install -e ".[aiosqlite,dev]"
pytest
# MySQL tests (requires a running MySQL server)
# MySQL 8.0 uses caching_sha2_password auth, which requires the cryptography package
pip install -e ".[aiomysql,dev]" cryptography
export KAKAORM_MYSQL_URL="mysql+aiomysql://root:password@localhost:3306/test_db"
pytest tests/test_mysql.py
Requirements
- Python 3.11+
- The appropriate driver for your database (
aiosqlite/asyncpg/psycopg[binary]/aiomysql) - For Pydantic v2 integration:
pip install pydantic(optional — the ORM core works without it)
License
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 kakaorm-0.4.0.tar.gz.
File metadata
- Download URL: kakaorm-0.4.0.tar.gz
- Upload date:
- Size: 64.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
27e151cf4ef99d699294e9efeb9157d4a6843a37a140737e7d91a75b735c5cae
|
|
| MD5 |
de45514a5f4a5f54d7e10d818e99ed18
|
|
| BLAKE2b-256 |
ce8c31541521aa6afd32bb4f771661c59db5b8d2a2e52c341ab9e30df631a56b
|
File details
Details for the file kakaorm-0.4.0-py3-none-any.whl.
File metadata
- Download URL: kakaorm-0.4.0-py3-none-any.whl
- Upload date:
- Size: 61.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.14.5
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
7362fb95ec59e8d0926df20efa38ab2441d59914bd961dc87546bf0fb7ed34b5
|
|
| MD5 |
671e7141bd12802b459081f7c8a0f783
|
|
| BLAKE2b-256 |
646feddd07f2fcb76b353ec9e4ca89a8e428f9f16675ffe395f6e91a3d5db723
|