Skip to main content

admin-litestar

A server-rendered admin panel for Litestar applications backed by SQLAlchemy. No build step, no JavaScript toolchain, no CDN at runtime — the CSS is hand-written and HTMX is vendored as package data.

It knows SQLAlchemy and Litestar. It knows nothing about your schema, your authentication, or where you keep your audit trail — those arrive through protocols you implement. A test in this repository fails if the package ever imports a host application.

Install

uv add admin-litestar

Requires Python 3.10+. Depends only on litestar, sqlalchemy and jinja2.

Usage

Declare a ModelSpec per model, implement three small protocols, and mount what Admin gives you.

from litestar import Litestar
from litestar.stores.memory import MemoryStore

from admin_litestar import (
    Admin,
    AdminConfig,
    DETAIL,
    EXPORT,
    LIST,
    ModelSpec,
    hash_password,
    verify_password,
)

INVOICE = ModelSpec(
    model=Invoice,
    slug="invoice",
    label="Invoices",
    group="Billing",
    list_columns=("id", "reference", "issued_at"),
    detail_columns=("id", "reference", "note", "issued_at"),
    capabilities=frozenset({LIST, DETAIL, EXPORT}),
    order_by="id",
    searchable=("reference",),
)


class Auth:
    """Decides who may enter the admin, and whether they still may."""

    async def authenticate(self, session, username, password):
        user = await lookup(session, username)
        if user and verify_password(password, user.admin_password):
            return user
        return None

    def identity_of(self, user):
        return user.id

    async def is_valid(self, session, actor_id) -> bool:
        return await still_permitted(session, actor_id)


admin = Admin(
    config=AdminConfig(path="/admin", static_path="/admin-static"),
    specs=[INVOICE],
    auth=Auth(),
    audit=YourAuditSink(),
    cache=lambda request: your_cache,
    session_factory=async_sessionmaker(engine),
)

app = Litestar(
    route_handlers=[admin.router(), admin.static_router()],
    template_config=admin.template_config(),
    middleware=[admin.session_config(MemoryStore()).middleware],
    csrf_config=admin.csrf_config(SECRET),
)

That yields a login page, a gated sidebar shell, and list / detail / delete / CSV-export routes for every spec that declares the matching capability.

The column boundary

ModelSpec distinguishes three kinds of column, and the distinction is enforced where statements are built rather than where values are rendered:

Field Behaviour
list_columns Loaded and shown in list views
detail_columns Loaded and shown in detail views
hidden_columns Permitted in detail views, never loaded by a list query
excluded_columns Never selected, rendered or exported, anywhere

List queries use load_only() over list_columns, so a hidden column is absent from the SQL itself. That matters when a column's SQLAlchemy type decrypts on load: a list page neither pays the cost nor can leak the value, even if a template is wrong. ModelSpec rejects contradictory declarations at construction, so a hidden column named in list_columns — or an excluded column named as searchable or filterable — is an error you get at import time, not a leak you find later.

Search

searchable columns match with ILIKE. exact_searchable columns match by equality, and search_transform is applied to the term first — which is how you search a keyed-digest column without this package knowing anything about your hashing:

ModelSpec(
    ...,
    exact_searchable=("iin_digest",),
    search_transform=your_digest_function,
)

Exact search takes precedence when both are declared, because a digest cannot be matched partially.

Pagination

Keyset, never OFFSET. The cursor is the last row's order_by value, coerced to the column's Python type — dates and datetimes are parsed with fromisoformat. A malformed or timezone-naive cursor is treated as absent and yields an unpaginated first page rather than an error, because cursors arrive from URLs and URLs get edited.

Custom pages

Generic tables cannot do everything. CustomPage lets a host contribute its own routes, rendered inside the same shell and listed in the same nav:

from admin_litestar import CustomPage

dashboard = CustomPage(
    slug="dashboard", label="Dashboard", group="Overview", handlers=[DashboardController]
)

Host templates take precedence over the package's, so AdminConfig(template_dirs=(...)) lets you override any template by name while extending base.html.

Authentication

The package owns the mechanism; you own the policy.

  • hash_password / verify_password use hashlib.scrypt with n=16384, r=8, p=1, dklen=32. The encoding is 86 characters, so it fits a String(128) column. Anything not in that format fails verification — there is no fallback to another scheme.
  • Login failures are counted per username and client IP, locking after 5 attempts for 15 minutes. Deliberately separate from any lockout counter on your own user rows, so admin brute-force cannot lock someone out of your main application.
  • Sessions are server-side over a store you supply. AuthBackend.is_valid is re-checked on every request, cached briefly, so revoking access takes effect in seconds rather than at session expiry.
  • CSRF protection covers every mutating route. Templates call {{ csrf_token() }}.

Design

Dark, near-monochrome, one amber accent, monospace for identifiers — chosen because admin data is mostly ids, hashes, addresses and timestamps, which align and scan far better in a monospaced column. Light and dark both ship, honouring prefers-color-scheme with an explicit data-theme override.

ModelSpec validates at construction: unknown column names, a hidden column listed in list_columns, an excluded column named as searchable, or an unknown capability all raise immediately rather than producing an admin that quietly misbehaves.

Status

Early. The API has one real consumer, so every protocol here is a considered guess about the second one. Expect 0.x releases to move interfaces, and pin exactly if that matters.

admin_litestar.__all__ is the compatibility promise. Deeper import paths work but carry none — see ARCHITECTURE.md.

Documentation

Licence

MIT. See LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

admin_litestar-0.1.0.tar.gz (50.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

admin_litestar-0.1.0-py3-none-any.whl (45.8 kB view details)

Uploaded Python 3

File details

Details for the file admin_litestar-0.1.0.tar.gz.

File metadata

  • Download URL: admin_litestar-0.1.0.tar.gz
  • Upload date:
  • Size: 50.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for admin_litestar-0.1.0.tar.gz
Algorithm Hash digest
SHA256 4a81acfacb786cc44a8e7a2e9caf8ad8ded14d571ce5a39ecda190273889c44f
MD5 e11828f3559eaa26a68f4ec661511e53
BLAKE2b-256 9ef0119da45302407eebfd6d16789b92fb3653e410babed17bdac89d1fcab40f

See more details on using hashes here.

Provenance

The following attestation bundles were made for admin_litestar-0.1.0.tar.gz:

Publisher: release.yml on adllkhan/admin-litestar

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file admin_litestar-0.1.0-py3-none-any.whl.

File metadata

  • Download URL: admin_litestar-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 45.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for admin_litestar-0.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 4f232c85969a754ce874f6f9954e7e9e69b23638b87f9080bc154cea2698e0b6
MD5 5b9ada7b5ad5ecca49da5cbcd8bfa6ae
BLAKE2b-256 1f324912cb19891b5d64cbd60de82db85865eacc2ce738517fb1b44bb3d22603

See more details on using hashes here.

Provenance

The following attestation bundles were made for admin_litestar-0.1.0-py3-none-any.whl:

Publisher: release.yml on adllkhan/admin-litestar

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

0.4.1

2 files

0.4.0

2 files

0.3.0

2 files

0.2.0

2 files

This release

0.1.0 This release

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page