Skip to main content

auen

Auto-generate FastAPI CRUD endpoints from SQLModel models.

Why auen?

Today writing repetitive code, like CRUD endpoints, became a single prompt to your Agent of Choice. So, why should we generate them from code instead? I believe, this is about standardization: Having the piece of mind that there is no artistic freedom in your CRUD endpoints, you'll expose to internal or external users. The ultimate goal here is to let you focus on the development of a data model beneath and let the interaction layer appear for free, at least for the set of endpoints that don't need any further customization.

Installation

pip install auen

Quickstart

from collections.abc import AsyncGenerator
from contextlib import asynccontextmanager

from fastapi import FastAPI
from sqlalchemy.ext.asyncio import create_async_engine
from sqlmodel import Field, SQLModel
from sqlmodel.ext.asyncio.session import AsyncSession

from auen import CrudRouterSpec, build_crud_routers

engine = create_async_engine("sqlite+aiosqlite:///bookstore.db")


class Author(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    name: str
    bio: str | None = None


class Book(SQLModel, table=True):
    id: int | None = Field(default=None, primary_key=True)
    title: str
    isbn: str
    author_id: int | None = Field(default=None, foreign_key="author.id")


@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
    async with engine.begin() as conn:
        await conn.run_sync(SQLModel.metadata.create_all)
    yield


async def get_session() -> AsyncGenerator[AsyncSession, None]:
    async with AsyncSession(engine) as session:
        yield session


app = FastAPI(lifespan=lifespan)
routers = build_crud_routers(
    specs=[
        CrudRouterSpec(model=Author),
        CrudRouterSpec(model=Book),
    ],
    session_dep=get_session,
)
for router in routers:
    app.include_router(router)

This spec-based interface is the canonical style. CrudRouterBuilder remains available as a fluent compatibility adapter.

from auen import CrudRouterSpec, Operation, build_crud_routers

routers = build_crud_routers(
    specs=[
        CrudRouterSpec(model=Author, operations={Operation.LIST, Operation.READ}),
        CrudRouterSpec(model=Book, operations={Operation.CREATE, Operation.LIST}),
    ],
    session_dep=get_session,
)
for router in routers:
    app.include_router(router)

Without auen, this requires writing five endpoint functions per model, request/response models, query parameter handling, error responses, and database session management — roughly 150 lines of code that looks the same in every project.

With auen, you get:

Method Path Description Method Path Description
POST /authors/ Create an author POST /books/ Create a book
GET /authors/ List authors GET /books/ List books
GET /authors/{id} Get an author GET /books/{id} Get a book
PATCH /authors/{id} Update an author PATCH /books/{id} Update a book
DELETE /authors/{id} Delete an author DELETE /books/{id} Delete a book

Explicit Schemas

For production, define explicit schemas to control exactly which fields are exposed. Without auen, you'd maintain these schemas and write all the endpoint code that uses them — auen handles the latter.

If you want fully derived schema bundles (including nested payload models), use:

from auen import derive_schemas

schemas = derive_schemas(Book)
# schemas.create / schemas.read / schemas.update
# schemas.nested_create / schemas.nested_update

For advanced use cases, you can access the full schema container directly:

from auen import derive_schema_container

container = derive_schema_container(Book)
# container.nested_update is used by PATCH /{id}/nested

Authentication

Auth integration is a common copy-paste pattern — every project writes the same dependency injection and per-operation gating. auen standardizes it.

from fastapi import Header, HTTPException
from auen import AuthConfig, CrudRouterBuilder


def get_current_user(authorization: str = Header()) -> str:
    if not authorization.startswith("Bearer "):
        raise HTTPException(status_code=401)
    return authorization[7:]  # return user identity


app.include_router(
    CrudRouterBuilder()
    .with_auth(AuthConfig(dependency=get_current_user))
    .build_routers(models=[Book], session_dep=get_session)[0]
)

Row-Level Policies

Row-level security is frequently reimplemented incorrectly — a missed filter on the LIST endpoint leaks data. auen makes it declarative so the policy is applied consistently across all operations.

from auen import AuthConfig, CrudRouterBuilder


class OwnerPolicy:
    def can_create(self, user, obj_in):
        return True

    def can_read(self, user, db_obj):
        return db_obj.author_id == user

    def can_update(self, user, db_obj, obj_in):
        return db_obj.author_id == user

    def can_delete(self, user, db_obj):
        return db_obj.author_id == user

    def filter_list_query(self, user, query):
        return query.where(Book.author_id == user)


app.include_router(
    CrudRouterBuilder()
    .with_auth(AuthConfig(dependency=get_current_user))
    .with_policy(OwnerPolicy())
    .build_routers(models=[Book], session_dep=get_session)[0]
)

Lifecycle Hooks

from auen import CrudRouterBuilder, HooksConfig


async def notify_new_book(session, book_obj, current_user):
    ...  # send email, log event, publish to queue, etc.


async def validate_isbn(session, obj_in, current_user):
    ...  # raise HTTPException to abort


app.include_router(
    CrudRouterBuilder()
    .with_hooks(
        HooksConfig(
            before_create=validate_isbn,
            after_create=notify_new_book,
        )
    )
    .build_routers(models=[Book], session_dep=get_session)[0]
)

Pagination & Filtering

Pagination and filtering are boilerplate that every API writes identically — offset/limit handling, sort-field validation, operator mapping. auen generates it from a declaration.

from auen import CrudRouterBuilder, PaginationConfig, FilterConfig, FilterFieldConfig

app.include_router(
    CrudRouterBuilder()
    .with_pagination(PaginationConfig(default_limit=20, max_limit=100))
    .with_filters(
        FilterConfig(
            fields={"title": FilterFieldConfig(ops=frozenset({"eq"}))},
            sort_fields=["title", "isbn"],
        )
    )
    .build_routers(models=[Book], session_dep=get_session)[0]
)

Selecting Operations

from auen import CrudRouterBuilder, Operation

app.include_router(
    CrudRouterBuilder()
    .with_operations({Operation.LIST, Operation.READ})  # read-only API
    .build_routers(models=[Book], session_dep=get_session)[0]
)

Development

# Install dependencies
uv sync

# Run tests
uv run pytest

# Format
uv run ruff format .

# Lint
uv run ruff check .

# Type check
uv run ty check

License

MIT

Metadata

Release files for auen 0.6.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for auen 0.6.0
File Size Uploaded Provenance
auen-0.6.0.tar.gz 149.3 kB Without provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for auen 0.6.0
File Interpreter ABI Platform
auen-0.6.0-py3-none-any.whl Python 3 none any Details

Total release size: 181.7 kB

Release files / auen-0.6.0.tar.gz

Download URL auen-0.6.0.tar.gz
Size 149.3 kB
Tags Source
SHA-256 checksum
How to use checksums
bbd7e69cba774476d15df512e7d53ea2ceb69597925f329e5ea4d0c85e06ad9a
BLAKE2b-256 checksum
How to use checksums
afe36dadc77752b3a73f3e993ffc218b58fdd1d82de2469d0a93eb8ac0a78898
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.10.7 {"installer":{"name":"uv","version":"0.10.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Release files / auen-0.6.0-py3-none-any.whl

Download URL auen-0.6.0-py3-none-any.whl
Size 32.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
783898fe70b38567ab2610b6da2282163fe32c944e5516d2912ecaf7809a5f2c
BLAKE2b-256 checksum
How to use checksums
45a9df90a09fdeb51b92aefbdc9b79bf1af6055f9f76f642755232a4c8e80d7c
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via uv/0.10.7 {"installer":{"name":"uv","version":"0.10.7","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Debian GNU/Linux","version":"13","id":"trixie","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}

Security for auen 0.6.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page