Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/guide/features.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,7 @@ Everything FastAPI Admin Kit offers, in one place.
| Ordering | Default sort order with clickable column headers | [Model Registration](model-registration.md) |
| Pagination | Offset, cursor, or dynamic strategies per model | [Pagination](pagination.md) |
| Custom Columns | `@column` decorator for computed columns with formatting, icons, width | [Model Registration](model-registration.md) |
| Custom Endpoints | `@endpoint` decorator for arbitrary FastAPI routes under `/<model>/...` with RBAC | [Model Registration](model-registration.md) |
| Column Export | CSV export support per column | [Model Registration](model-registration.md) |

---
Expand Down
67 changes: 67 additions & 0 deletions docs/guide/model-registration.md
Original file line number Diff line number Diff line change
Expand Up @@ -400,6 +400,73 @@ class ProductAdmin(ModelAdmin):
| `exportable` | `bool` | Include in CSV export (default: `True`) |
| `icon` | `str` | Material icon name |

## Custom API Endpoints

Use the `@endpoint()` decorator to add arbitrary FastAPI routes to a model's
admin router. Every endpoint is served under `/<model_name>/<path>` (mounted
under the admin prefix), so a `health-check` endpoint on the `Product` model
is reachable at `/admin/products/health-check`.

```python
from fastapi_admin_kit import endpoint

@admin.register(Product)
class ProductAdmin(ModelAdmin):
@endpoint(
path="/health-check",
methods=["GET"],
tags=["monitoring"],
description="Health check endpoint",
summary="Health summary",
response_description="Healthy status",
permission="view",
)
async def health_check(self, request):
return {"status": "healthy"}

@endpoint(path="/stats", methods=["GET"])
async def stats(self, request, days: int = 7):
return {"stats": {"days": days}}
```

The `request` parameter is injected automatically (annotated `Request` or a
bare `request` argument both work); other parameters are handled by FastAPI as
usual (query/path params, bodies, etc.).

### Endpoint Options

| Option | Type | Required | Default | Description |
|--------|------|----------|---------|-------------|
| `path` | `str` | Yes | - | Path within the model router (e.g. `/health-check`) |
| `methods` | `list[str]` | No | `["GET"]` | HTTP methods to expose |
| `tags` | `list[str]` | No | `[]` | OpenAPI tags (appended to the model tag) |
| `description` | `str` | No | `""` | Route description |
| `name` | `str` | No | auto-generated | Route name (defaults to `<model>_<method>`) |
| `dependencies` | `list[Any]` | No | `[]` | Extra FastAPI `Depends()` dependencies |
| `status_code` | `int` | No | `200` | Response status code |
| `response_model` | `Type[BaseModel]` | No | `None` | Response schema for validation/docs |
| `summary` | `str` | No | `""` | OpenAPI summary |
| `response_description` | `str` | No | `""` | OpenAPI response description |
| `permission` | `str` | No | `None` | RBAC action enforced via `require_permission` |

### RBAC

Setting `permission` enforces the same RBAC used by the built-in routes (a
`require_permission("<model>", "<permission>")` dependency). You can also pass
arbitrary dependencies directly:

```python
from fastapi import Depends
from fastapi_admin_kit.auth.dependencies import require_permission

@endpoint(
path="/stats",
dependencies=[Depends(require_permission("products", "view"))],
)
async def stats(self, request):
pass
```

## Customizing Built-in Admin Models

FastAPI Admin Kit ships with default admin classes for built-in models (users, roles, audit logs, etc.). You can customize these by inheriting from the default classes.
Expand Down
3 changes: 2 additions & 1 deletion fastapi_admin_kit/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""FastAPI Admin Kit — Drop-in admin panel for FastAPI + SQLAlchemy apps."""

from fastapi_admin_kit.admin import Admin
from fastapi_admin_kit.admin.decorators import column
from fastapi_admin_kit.admin.decorators import column, endpoint
from fastapi_admin_kit.auth.mixins import AuthModelMixin
from fastapi_admin_kit.config import DatabaseConfig, DatabaseType
from fastapi_admin_kit.exceptions import ConfigError
Expand Down Expand Up @@ -74,6 +74,7 @@
"RegisteredModel",
"ModelAdmin",
"column",
"endpoint",
"BuiltNavGroup",
"BuiltNavItem",
"DefaultSidebarBuilder",
Expand Down
80 changes: 79 additions & 1 deletion fastapi_admin_kit/admin/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,33 @@
from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass
from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any

if TYPE_CHECKING:
from pydantic import BaseModel


@dataclass
class EndpointOptions:
"""Metadata for @endpoint() decorator."""

path: str
methods: list[str] = field(default_factory=lambda: ["GET"])
tags: list[str] = field(default_factory=list)
description: str = ""
name: str = ""
dependencies: list[Any] = field(default_factory=list)
status_code: int = 200
response_model: type[BaseModel] | None = None
summary: str = ""
response_description: str = ""
permission: str | None = None
include_in_schema: bool = True

def __call__(self, func: Callable) -> Callable:
func._admin_endpoint = self
return func


@dataclass
Expand Down Expand Up @@ -68,3 +94,55 @@ def price_display(self, obj):
exportable=exportable,
icon=icon,
)


def endpoint(
path: str,
methods: list[str] | None = None,
tags: list[str] | None = None,
description: str = "",
name: str = "",
dependencies: list[Any] | None = None,
status_code: int = 200,
response_model: type[BaseModel] | None = None,
summary: str = "",
response_description: str = "",
permission: str | None = None,
include_in_schema: bool = True,
) -> EndpointOptions:
"""Decorator to register a custom FastAPI endpoint on a ModelAdmin.

Endpoints are auto-registered on the model's admin router by
``build_model_router()`` via ``APIRouter.add_api_route()``, keeping full
FastAPI configuration support (path, methods, tags, dependencies,
status code, response model, ...).

Usage::

from fastapi_admin_kit import endpoint

class ProductAdmin(ModelAdmin):
@endpoint(
path="/health-check",
methods=["GET"],
tags=["monitoring"],
description="Health check endpoint",
permission="view",
)
async def health_check(self, request):
return {"status": "healthy"}
"""
return EndpointOptions(
path=path,
methods=methods or ["GET"],
tags=tags or [],
description=description,
name=name,
dependencies=dependencies or [],
status_code=status_code,
response_model=response_model,
summary=summary,
response_description=response_description,
permission=permission,
include_in_schema=include_in_schema,
)
5 changes: 4 additions & 1 deletion fastapi_admin_kit/modeladmin.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from typing import TYPE_CHECKING, Any

from fastapi_admin_kit.admin.decorators import column
from fastapi_admin_kit.admin.decorators import column, endpoint
from fastapi_admin_kit.form.types import ExtraField, FieldMeta

if TYPE_CHECKING:
Expand Down Expand Up @@ -113,6 +113,9 @@ def get_ordering(request_params: dict, admin_ordering: list[str] | None) -> list
# Decorator for custom column display
column = staticmethod(column)

# Decorator for custom FastAPI endpoints
endpoint = staticmethod(endpoint)

# ── Standalone router export (no admin.register required) ───────

def export_api_route(self, model: Any, prefix: str = "") -> Any:
Expand Down
66 changes: 66 additions & 0 deletions fastapi_admin_kit/router.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,41 @@
)


def _endpoint_handler(fn):
"""Prepare an ``@endpoint`` method for FastAPI registration.

FastAPI only injects the ``Request`` object into parameters annotated as
``Request``. To honour the documented ``async def health_check(self, request)``
usage, any parameter literally named ``request`` without an annotation is
re-annotated as ``Request`` via a thin forwarding wrapper (other parameters,
e.g. query/path params, are preserved).
"""
import inspect

from fastapi import Request

sig = inspect.signature(fn)
params = list(sig.parameters.values())
if not any(p.name == "request" and p.annotation is inspect.Parameter.empty for p in params):
return fn

new_params = [
p.replace(annotation=Request)
if p.name == "request" and p.annotation is inspect.Parameter.empty
else p
for p in params
]
new_sig = sig.replace(parameters=new_params)

async def wrapper(*args, **kwargs):
return await fn(*args, **kwargs)

wrapper.__signature__ = new_sig
wrapper.__name__ = getattr(fn, "__name__", "endpoint")
wrapper.__doc__ = getattr(fn, "__doc__", None)
return wrapper


def build_model_router(registered: RegisteredModel, *, force: bool = False) -> APIRouter | None:
"""Build the HTML admin router for a model.

Expand Down Expand Up @@ -367,6 +402,37 @@ async def validate_field_endpoint(
},
)

# ── Custom @endpoint routes ────────────────────────────────────
# Registered before the ``/{id}`` catch-all so custom paths (e.g.
# ``/health-check``) are never swallowed by the edit-view route.

for name in dir(admin):
if name.startswith("__"):
continue
fn = getattr(admin, name, None)
opts = getattr(fn, "_admin_endpoint", None)
if opts is None:
continue

dependencies = list(opts.dependencies or [])
if opts.permission:
dependencies.append(Depends(require_permission(registered.table_name, opts.permission)))

router.add_api_route(
opts.path,
_endpoint_handler(fn),
methods=opts.methods or ["GET"],
tags=opts.tags or None,
description=opts.description or None,
name=opts.name or f"{registered.table_name}_{name}",
dependencies=dependencies or None,
status_code=opts.status_code,
response_model=opts.response_model,
summary=opts.summary or None,
response_description=opts.response_description or None,
include_in_schema=opts.include_in_schema,
)

router.add_api_route(
"/{id}",
edit_v.html_response,
Expand Down
5 changes: 4 additions & 1 deletion fastapi_admin_kit/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from fastapi import APIRouter, Request
from fastapi.responses import HTMLResponse

from fastapi_admin_kit.admin.decorators import column
from fastapi_admin_kit.admin.decorators import column, endpoint

if TYPE_CHECKING:
from fastapi_admin_kit.registry import RegisteredModel
Expand Down Expand Up @@ -39,6 +39,9 @@ class ModelAdmin:
# Decorator for custom column display
column = staticmethod(column)

# Decorator for custom FastAPI endpoints
endpoint = staticmethod(endpoint)

def __str__(self, obj: Any) -> str:
"""How to display an object in dropdowns/links."""
return str(getattr(obj, "name", None) or getattr(obj, "title", None) or f"#{obj.id}")
Expand Down
Loading
Loading