# auen

**Au**to-generate FastAPI CRUD **en**dpoints 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

```bash
pip install auen
```

## Quickstart

```python
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 CrudRouterBuilder

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)
app.include_router(CrudRouterBuilder.for_model(Author, get_session).build())
app.include_router(CrudRouterBuilder.for_model(Book, get_session).build())
```

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 update payload models), use:

```python
from auen import derive_schema_container

container = derive_schema_container(Book)
# container.create / container.read / container.update / container.nested_update
```

## Authentication

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

```python
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.for_model(Book, get_session)
    .with_auth(AuthConfig(dependency=get_current_user))
    .build()
)
```

## 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.

```python
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.for_model(Book, get_session)
    .with_auth(AuthConfig(dependency=get_current_user))
    .with_policy(OwnerPolicy())
    .build()
)
```

## Lifecycle Hooks

```python
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.for_model(Book, get_session)
    .with_hooks(
        HooksConfig(
            before_create=validate_isbn,
            after_create=notify_new_book,
        )
    )
    .build()
)
```

## 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.

```python
from auen import CrudRouterBuilder, PaginationConfig, FilterConfig, FilterFieldConfig

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

## Selecting Operations

```python
from auen import CrudRouterBuilder, Operation

app.include_router(
    CrudRouterBuilder.for_model(Book, get_session)
    .with_operations({Operation.LIST, Operation.READ})  # read-only API
    .build()
)
```

## Development

```bash
# 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
