Auto-generated admin UI for HawkAPI + hawkapi-sqlalchemy models. Drop your model classes in and get list / detail / create / edit / delete views mounted under /admin — no boilerplate, no React, no JSON-schema duplication.
pip install hawkapi-adminfrom hawkapi import HawkAPI
from hawkapi_sqlalchemy import Base, TimestampMixin, init_database
from sqlalchemy.orm import Mapped, mapped_column
from hawkapi_admin import Admin, ModelResource, init_admin
class User(Base, TimestampMixin):
__tablename__ = "users"
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
email: Mapped[str] = mapped_column(unique=True)
name: Mapped[str] = mapped_column(default="")
app = HawkAPI()
init_database(app, url="postgresql+asyncpg://…/myapp")
admin = init_admin(app, title="My App")
admin.register(User) # simplest formVisit /admin — you get the index page, /admin/user (list), /admin/user/new (create form), /admin/user/{id} (detail), /admin/user/{id}/edit, and POST /admin/user/{id}/delete. All with sane widgets picked from each column's SQLAlchemy type.
from hawkapi_admin import ModelResource
admin.register(ModelResource(
model=User,
label="Account",
label_plural="Accounts",
icon="👤",
list_display=("id", "email", "created_at"),
list_search=("email", "name"),
form_fields=("email", "name"),
readonly_fields=("created_at",),
page_size=25,
can_delete=False,
))| Option | Default | Effect |
|---|---|---|
name |
lowercased class name | URL slug (/admin/<name>) |
label |
class name | Heading on detail / form |
label_plural |
label + "s" |
Nav label & list-page heading |
icon |
"" |
Prepended to nav label |
list_display |
every column | Columns shown on the list page |
list_search |
() |
Columns searched by ?q=... (LIKE) |
form_fields |
every non-PK column | Columns shown in the form |
readonly_fields |
() |
Columns rendered but not editable |
page_size |
50 |
Rows per list page |
can_create / can_update / can_delete |
True |
Toggles the corresponding routes off |
hawkapi-admin picks an input widget per column type, automatically:
bool→ checkboxint/float→<input type="number">date/datetime→ matching native inputString(length > 500)→ textareaEnum→<select>with the declared choices- everything else →
<input type="text">
The admin panel is fail-closed. If you do not pass an auth callable to init_admin(...) (or Admin(...)), every admin request is denied with HTTP 401 — the panel no longer silently allows access. A UserWarning is emitted at construction and a logger.warning at attach() to flag the misconfiguration.
You must pass an auth callable to enable access. It is an async function that receives the request and raises HTTPException(401)/HTTPException(403) to reject it (return None to allow). It runs at the top of every admin route. Combine with hawkapi-auth:
from hawkapi import HTTPException
from hawkapi_auth import requires_scopes
from hawkapi_admin import init_admin
async def admin_auth(request):
# raises 401/403 if the JWT is missing / lacks the scope
await requires_scopes("admin:read")(request)
admin = init_admin(app, title="My App", auth=admin_auth)
admin.register(User)Or hand-roll the check:
async def admin_auth(request):
if request.headers.get("x-admin-token") != "…":
raise HTTPException(401)
admin = init_admin(app, auth=admin_auth)- Detail views render only
detail_fields()— derived fromlist_displaywhen you configure it — so columns hidden from the list page are not exposed on the detail page either. With nolist_display, the detail view falls back to every mapped column. - Mass-assignment protection. Field names that look sensitive or authorization-bearing (
password,secret,token,key,hash,role,admin,superuser,permission,privilege,staff,scope) are forced read-only unless you explicitly opt them intoform_fields. This prevents privilege escalation via the edit form (e.g. settingis_adminorrole). - CSRF cookie hardening. The
hawkapi_admin_csrfcookie is nowHttpOnlywith aMax-Age(tokens rotate rather than living indefinitely), in addition toSecureandSameSite=Lax. - Security headers. Every admin HTML response carries
X-Frame-Options: DENY,X-Content-Type-Options: nosniff,Referrer-Policy: same-origin, and a restrictiveContent-Security-Policy.
The bundled CSS is roughly 60 lines, prefers system colors (light + dark mode), and lives inline in _base.html — copy that template into your own templates/ directory if you want to restyle. Jinja extends "_base.html" keeps working as long as the same blocks (title, content) are defined.
git clone https://github.com/Hawk-API/hawkapi-admin.git
cd hawkapi-admin
uv sync --extra dev
uv run pytest -q
uv run ruff check . && uv run ruff format --check .
uv run pyright src/MIT.