Skip to content

Reference

The classes you use most, with their signatures and docstrings.

The admin

adminsite.Admin

Admin(
    source: Database | SessionSource,
    *,
    title: str = "Admin",
    banner: str = "",
    views: Sequence[ModelView | type[ModelView]] = (),
    inspector: SQLAlchemyInspector | None = None,
    fields: FieldRegistry | None = None,
    template_dirs: Sequence[str | Path] = (),
    auth: AuthProvider | None = None,
    secret_key: str = "",
    audit: AuditStore | bool = False,
    saved_views: SavedViews | bool = False,
    session_cookie: str | None = None,
    pages: Sequence[AdminPage | type[AdminPage]] = (),
    plugins: Sequence[Plugin] = (),
    dashboard: Sequence[Widget] | None = None,
    api: bool = False,
    session_https_only: bool = False,
    session_max_age: int | None = 14 * 24 * 3600,
    language: str = "en",
    languages: Sequence[str] = (),
    translations: Mapping[str, Mapping[str, str]]
    | None = None,
)

The admin itself: a small ASGI app you mount into your own.

admin = Admin(engine, title="Acme")
admin.add_view(OrderView)
app.mount("/admin", admin)

add_view

add_view(view: ModelView | type[ModelView]) -> ModelView

Register a model with the admin.

add_page

add_page(page: AdminPage | type[AdminPage]) -> AdminPage

Add a page of your own, served at /-/ and its name.

use

use(plugin: Plugin) -> Plugin

Let a plugin add what it brings.

add_route

add_route(
    path: str,
    endpoint: Endpoint,
    *,
    methods: Sequence[str] = ("GET",),
    name: str | None = None,
    guarded: bool = True,
) -> None

Answer one more path, called as endpoint(admin, request).

The path has to start with /-/, which no model name can take, and the route sits behind the admin's sign in unless guarded is off.

add_static

add_static(name: str, directory: str | Path) -> None

Serve a folder of files at /-/static/ and the name.

add_template_dir

add_template_dir(directory: str | Path) -> None

Look for templates in one more folder, before the built in ones.

add_stylesheet

add_stylesheet(href: str) -> None

Load a stylesheet on every page. A relative path starts at the admin.

add_script

add_script(src: str) -> None

Load a script on every page. A relative path starts at the admin.

render_template async

render_template(
    name: str,
    request: Request,
    context: dict[str, Any] | None = None,
    status_code: int = 200,
) -> Response

Render a template of your own, found in the admin's template dirs.

Views

adminsite.ModelView

ModelView(
    inspector: SQLAlchemyInspector | None = None,
    registry: FieldRegistry | None = None,
)

How one model appears in the admin.

Set the class attributes to describe the list and the form. Override the get_ methods when the answer depends on who is asking.

get_list_display

get_list_display(request: Any = None) -> tuple[str, ...]

The columns the list shows.

get_search_fields

get_search_fields(request: Any = None) -> tuple[str, ...]

The paths the search box looks in.

get_filters

get_filters(request: Any = None) -> tuple[SQLFilter, ...]

The filters offered beside the list.

get_ordering

get_ordering(request: Any = None) -> tuple[Sort, ...]

The order the list starts in.

get_form_fields

get_form_fields(
    request: Any = None, record: Any = None
) -> tuple[str, ...]

The fields the form shows, in order.

get_readonly_fields

get_readonly_fields(
    request: Any = None, record: Any = None
) -> tuple[str, ...]

The fields shown but not editable, named here or by themselves.

A primary key is readonly by its nature, but a form that names one means to set it, so a key stays editable unless it is named here.

get_actions

get_actions(request: Any = None) -> tuple[Action, ...]

The actions this view offers, in the order they appear.

get_inlines

get_inlines(
    request: Any = None, record: Any = None
) -> tuple[Inline, ...]

The child records edited inside the form.

get_column_choices

get_column_choices(request: Any = None) -> tuple[str, ...]

The columns the picker offers: the list's own, then the extras.

allows async

allows(
    action: Permission | str,
    *,
    request: Any = None,
    record: Any = None,
) -> bool

Whether the current user may do this, to this record.

scope_query

scope_query(
    statement: Select[Any], *, request: Any = None
) -> Select[Any]

Narrow every read to the rows this user may see.

This runs on the list, the count, a single record, an export and a bulk action, so a row can never leak through a path that forgot to check.

before_save async

before_save(context: SaveContext) -> None

Runs before the values are written.

Change context.values, or context.set("slug", ...), to store something other than what was submitted. Raise RefusedError to refuse the save, naming a field to put the message beside it.

after_save async

after_save(context: SaveContext) -> None

Runs after the flush, while the transaction is still open.

before_delete async

before_delete(context: DeleteContext) -> None

Runs before a record is deleted. Raise to refuse the delete.

after_delete async

after_delete(context: DeleteContext) -> None

Runs after the delete, while the transaction is still open.

title_of

title_of(record: Any) -> str

Name a record, for a heading or a link to it.

adminsite.Inline dataclass

Inline(
    name: str,
    fields: Sequence[str] = (),
    readonly_fields: Sequence[str] = (),
    label: str = "",
    extra: int = 1,
    can_delete: bool = True,
    display_template: str = "",
)

Child records edited inside their parent's form, such as order lines.

class OrderView(ModelView, model=Order):
    inlines = (Inline("items", fields=("product", "quantity", "unit_price")),)

The name is a relationship on the parent that holds many records. Each child shows as a row of inputs, with a box to delete it, and new rows can be added in the form.

input_name

input_name(index: int | str, path: str) -> str

The name a child's input carries in the submitted form.

adminsite.CountMode

Bases: StrEnum

How hard to work to report how many records match.

adminsite.Pagination

Bases: StrEnum

How the list moves from one page to the next.

adminsite.views.writing.SaveContext dataclass

SaveContext(
    session: SessionAdapter,
    record: Any,
    values: dict[str, Any],
    created: bool,
    request: Any = None,
)

What a save hook is given. Everything here is inside one transaction.

In before_save the values have not been written onto the record yet, so that is where to change them: context.values is the dictionary that is about to be applied, and set writes into it. Setting an attribute on context.record there would be overwritten a moment later by the value from the form.

set

set(path: str, value: Any) -> None

Change a value before it is stored.

adminsite.views.writing.DeleteContext dataclass

DeleteContext(
    session: SessionAdapter,
    record: Any,
    request: Any = None,
)

What a delete hook is given, inside the same transaction.

Actions

adminsite.actions.action.action

action(
    label: str = "",
    *,
    name: str = "",
    confirm: str = "",
    permission: str = Permission.EDIT,
    dangerous: bool = False,
    inputs: Sequence[Field] = (),
    on: str = ON_SELECTION,
) -> Callable[[Handler], Handler]

Mark a method as an action.

@action(
    "Mark as shipped",
    confirm="Mark the chosen orders as shipped?",
    inputs=[ChoiceField("carrier", choices=CARRIERS, required=True)],
)
async def ship(self, selection: Selection, carrier: str) -> str:
    changed = await selection.update(status="shipped", carrier=carrier)
    return f"{changed} orders sent with {carrier}."

Each input is asked for in a dialog before the action runs, checked like a form field, and passed to the method by name.

on says what it acts on, and what the method is given:

  • "selection", the default: the rows the user ticked, as a Selection.
  • "record": one record, from its row in the list or from its page, as (record, session).
  • "view": nothing in particular, as (session). For work about the whole table, such as fetching from another system.

A method returns the message to show, or a response to send instead, such as a file to download.

adminsite.actions.Selection dataclass

Selection(
    view: ModelView,
    session: SessionAdapter,
    spec: QuerySpec,
    keys: Sequence[str] = (),
    everything: bool = False,
    request: Any = None,
    changes: dict[str, dict[str, Change]] = dict(),
)

The rows an action runs over.

Either the rows that were ticked, or every row the current search and filters match. The second kind is never loaded into memory, so an action over a large table stays one statement.

update async

update(**values: Any) -> int

Change every row this covers, in one statement.

This does not run the save hooks, because it never loads the records. Use records when the hooks matter.

delete async

delete() -> int

Delete every row this covers, in one statement.

count async

count() -> int

How many rows this covers.

records async

records() -> list[Any]

Load the records, for work that needs each one in turn.

statement

statement() -> Select[Any]

A statement selecting the primary keys this covers.

covered_keys async

covered_keys() -> list[str]

The keys of the records this covers, written as the URLs write them.

Filters

adminsite.backends.sqlalchemy.SQLFilter

SQLFilter(
    name: str,
    *,
    path: str | None = None,
    label: str | None = None,
)

Bases: Filter

A filter that narrows a SQLAlchemy statement.

Write a custom filter by subclassing this and returning a condition. Override apply instead when the filter needs to change the statement itself, for example to add a join.

condition

condition(
    value: FilterValue, repository: SQLAlchemyRepository
) -> ColumnElement[bool] | None

The condition this filter adds, or nothing to leave the list be.

apply

apply(
    statement: Select[Any],
    value: FilterValue,
    repository: SQLAlchemyRepository,
) -> Select[Any]

Narrow the statement with this filter's condition.

adminsite.filters.FilterOption dataclass

FilterOption(
    value: str, label: str, count: int | None = None
)

One choice a filter offers, with how many records it would match.

adminsite.filters.FilterValue dataclass

FilterValue(name: str, values: tuple[str, ...])

What the user picked for one filter.

first property

first: str

The first value, which is all a single choice filter uses.

Fields

adminsite.FieldOptions

FieldOptions(name: str, **changes: Any)

Changes to the field adminsite worked out for a path.

Put it in a view's fields where only a label, a line of help or a length changes and the field itself is already right:

class ProductView(ModelView, model=Product):
    fields = (
        FieldOptions("name", label="Product name"),
        FieldOptions("description", help_text="Shown on the shop page."),
    )

It takes whatever the field takes, so a link can be given a display_template without naming its target again.

adminsite.Computed

Computed(
    name: str,
    getter: Callable[[Any], Any],
    *,
    needs: Sequence[str] = (),
    **options: Any,
)

Bases: Field

A value the view works out from a record, rather than a column.

class ProductView(ModelView, model=Product):
    list_display = ("name", "capacity")
    fields = (
        Computed(
            "capacity",
            lambda product: f"{len(product.slots)}/{product.limit}",
            label="Capacity",
            needs=("slots",),
        ),
    )

It shows in the list, on the record page and in the export, and is never written, sorted or filtered. needs names the paths the function reads, so they are loaded with the page instead of one query per row.

text_for

text_for(record: Any, value: Any = None) -> str

Work the value out from the record, as text.

display

display(value: Any) -> str

An empty value shows as nothing, anything else as text.

adminsite.Html

Bases: Markup

Text written into the page as markup instead of being escaped.

A field that returns it can put a link, a badge or an icon in a cell:

Computed(
    "tracking",
    lambda order: Html('<a href="{}">Track</a>').format(order.tracking_url),
    needs=("tracking_url",),
)

Everything interpolated with format or % is escaped, so a value from the database cannot carry markup of its own into the page. Anything written straight into the string is not, so keep that to markup you wrote.

The CSV export and the JSON API send the text without the tags, since markup belongs on the page and not in a spreadsheet.

adminsite.fields.JSONField

JSONField(
    name: str,
    *,
    label: str | None = None,
    required: bool = False,
    readonly: bool = False,
    help_text: str = "",
    max_length: int | None = None,
    default: Any = None,
    format: str | None = None,
    secret: bool | None = None,
)

Bases: Field

A JSON column: an object, a list, or any other JSON document.

Chosen for JSON columns by itself. The form edits the document in a box, pretty printed, and a malformed one comes back as an error on the field with the text still there.

display

display(value: Any) -> str

The document on one line, cut short where a cell needs it.

serialize

serialize(value: Any) -> str

The document laid out over several lines, for the box.

Text goes back as it was typed, so a document that failed to parse comes back to its writer exactly as they left it.

to_python

to_python(text: str) -> Any

Read the text as a JSON document.

adminsite.fields.FileField

FileField(
    name: str,
    *,
    storage: FileStorage,
    accept: str = "",
    max_size: int = 10 * MEGABYTE,
    **options: Any,
)

Bases: Field

An uploaded file, kept in a storage, its key kept in a string column.

FileField("invoice", storage=LocalStorage("uploads"), accept=".pdf")

display

display(value: Any) -> str

The file's original name.

content_type

content_type(key: str) -> str

The type of a stored file, guessed from its name.

parse_upload

parse_upload(
    raw: Any, *, remove: bool, has_file: bool
) -> Any

Read a file input: a new file, None to remove, or UNCHANGED.

has_file says whether the record already holds a file, since a required field is satisfied by the one already stored.

check

check(upload: UploadFile) -> None

Refuse a file that is too big or not of an accepted type.

accepts

accepts(upload: UploadFile) -> bool

Whether the upload matches accept, as a browser reads it.

adminsite.fields.ImageField

ImageField(
    name: str,
    *,
    storage: FileStorage,
    accept: str = "image/png,image/jpeg,image/gif,image/webp",
    max_size: int = 5 * MEGABYTE,
    **options: Any,
)

Bases: FileField

An uploaded picture, shown as a thumbnail in the list and the form.

Takes PNG, JPEG, GIF and WebP, checked by the file's first bytes, not just its name. SVG is left out on purpose: it can carry script.

check

check(upload: UploadFile) -> None

Refuse anything that is not really one of the image types.

adminsite.files.FileStorage

Where uploaded files go.

Subclass it to keep files somewhere else, such as S3: save returns the key kept in the column, and response answers a request for the file, for example with a redirect to a signed URL.

save async

save(upload: UploadFile) -> str

Store an upload and return the key to keep in the column.

delete async

delete(key: str) -> None

Remove a stored file. A missing file is not an error.

url

url(key: str) -> str

A public address for the file, or nothing to serve it through the admin.

response async

response(key: str, content_type: str = '') -> Response

Answer a request for the file, made through the admin.

adminsite.files.LocalStorage

LocalStorage(
    directory: str | Path, *, url_prefix: str = ""
)

Bases: FileStorage

Keeps files in a folder on this machine.

uploads = LocalStorage("uploads")

Files are served through the admin, behind its sign in, unless you give url_prefix for a place your application already serves them from. Each file gets a key such as 2026/09/k3j9x2-invoice.pdf: the month, a random part so names never clash, and the original name.

url

url(key: str) -> str

The public address, when the files are served elsewhere.

path_for

path_for(key: str) -> Path

Where a key lives on disk, refusing any key that leaves the folder.

Pages, plugins and the dashboard

adminsite.AdminPage

AdminPage()

A page of your own inside the admin, such as a report or a settings form.

class SalesReport(AdminPage):
    label = "Sales report"
    group = "Reports"
    template = "reports/sales.html"

    async def get_context(self, request):
        async with self.admin.database.session() as session:
            total = await session.scalar(select(func.sum(Order.total)))
        return {"total": total}

The template lives in one of the admin's template_dirs and usually extends adminsite/base.html, so the page keeps the sidebar and header. It is served at /-/sales_report and listed in the sidebar and the command palette.

allows async

allows(request: Request) -> bool

Whether this user may open the page. Everyone may, unless you say.

get_context async

get_context(request: Request) -> dict[str, Any]

The values the template needs, on top of what every page has.

get async

get(request: Request) -> Response

Show the page. Override it to answer with anything else.

post async

post(request: Request, form: dict[str, Any]) -> Response

Handle a form posted to the page, already checked for its token.

adminsite.Plugin

Something that adds to an admin, packaged so it can be shared.

A plugin gets the admin once, in setup, and adds what it brings with the same methods a project uses:

class Reports(Plugin):
    name = "reports"

    def setup(self, admin):
        admin.add_template_dir(Path(__file__).parent / "templates")
        admin.add_static("reports", Path(__file__).parent / "static")
        admin.add_stylesheet("-/static/reports/reports.css")
        admin.add_page(SalesReport)
        admin.add_route("/-/reports/export", export_sales)


admin = Admin(engine, plugins=[Reports()])

setup

setup(admin: Admin) -> None

Add the plugin's views, pages, routes, templates and assets.

adminsite.Widget

One card on the overview page.

Subclass it for a card of your own: set template to a template in your template_dirs and return what it needs from load. The template gets widget and data.

allows async

allows(admin: Admin, request: Request) -> bool

Whether this user sees the card. Everyone does, unless you say.

load async

load(admin: Admin, request: Request) -> Any

Read what the card shows.

adminsite.Stat

Stat(
    title: str,
    value: Source,
    *,
    previous: Source | None = None,
    format: str = "{:,}",
    hint: str = "",
    link: str = "",
    width: int = 1,
)

Bases: Widget

A single number, such as revenue this month, with an optional change.

Stat(
    "Revenue this month",
    select(func.sum(Order.total)).where(Order.created_at >= month_start),
    previous=select(func.sum(Order.total)).where(...last month...),
    format="€{:,.2f}",
)

load async

load(admin: Admin, request: Request) -> StatData

Read the number, and the one it is compared with.

adminsite.Chart

Chart(
    title: str,
    rows: Source,
    *,
    kind: str = "bar",
    format: str = "{:,}",
    width: int = 2,
)

Bases: Widget

Bars or a line over labelled values, drawn as plain SVG.

The source gives rows of a label and a value, such as orders per day:

day = func.date(Order.created_at)
Chart(
    "Orders per day",
    select(day, func.count()).group_by(day).order_by(day),
    kind="line",
)

load async

load(admin: Admin, request: Request) -> ChartData

Read the rows and place them on the canvas.

tick_label

tick_label(value: float) -> str

A value on the axis, without cents where it is a whole number.

adminsite.RecentRecords

RecentRecords(
    title: str,
    view: str,
    *,
    sort: str = "",
    detail: str = "",
    limit: int = 5,
    width: int = 2,
)

Bases: Widget

The latest records of a view, linking to each.

RecentRecords("Latest orders", "orders", sort="-created_at", detail="total")

It reads through the view, so the view's permissions and scope apply.

allows async

allows(admin: Admin, request: Request) -> bool

Shown to whoever may open the view.

load async

load(admin: Admin, request: Request) -> list[RecentItem]

Read the latest records through the view.

adminsite.ModelCounts

Bases: Widget

How many records each view holds: the overview when nothing else is set.

load async

load(admin: Admin, request: Request) -> list[CountItem]

Count the records of every view this user may open, within its scope.

Saved views

adminsite.SavedViews

SavedViews(
    source: Database | SessionSource | str = DEFAULT_URL,
    *,
    create_table: bool | None = None,
)

Bases: Store

Where people keep the lists they come back to.

Like the audit log, the views go to a SQLite file of their own by default. Pass your engine to keep them in your database; the table is then adminsite_saved_views on saved_view_metadata.

visible_to async

visible_to(view: str, owner: str | None) -> list[SavedView]

A person's own views of a list, and the ones others shared.

save async

save(saved: SavedView) -> SavedView

Keep a view, and return it with its id.

delete async

delete(key: int, owner: str | None) -> bool

Remove a view, if it belongs to this person.

Translations

adminsite.i18n.gettext

gettext(text: str, **values: Any) -> str

Translate a piece of English text into the current language.

A project's own translations win over the built in ones, and text with no translation stays in English rather than disappearing.

Signing in

adminsite.auth.AuthProvider

Decides who may use the admin.

Subclass it and write verify. The session handling here is enough for most projects, and load_user is where you turn the key kept in the session back into whatever your application calls a user.

verify async

verify(username: str, password: str) -> Any | None

Return the user for these details, or nothing.

To have the audit log say why an attempt failed, raise SignInRefused("This account is switched off.", user=account) instead of returning nothing.

load_user async

load_user(key: str) -> Any | None

Turn the key kept in the session back into a user.

authenticate_token async

authenticate_token(token: str) -> Any | None

Return the user an API token belongs to, or nothing.

The JSON API calls this for Authorization: Bearer <token>. Nobody gets in this way until you write it, for example by looking the token up in a table of API keys.

identity

identity(user: Any) -> str

The key to keep in the session for this user.

current_user async

current_user(request: Request) -> Any | None

Who is signed in, if anyone.

sign_in async

sign_in(
    request: Request, username: str, password: str
) -> Any | None

Check the details and remember the user.

sign_in_failed async

sign_in_failed(request: Request, username: str) -> str

Called when a sign in fails. Returns what to tell the person.

Override it to record the attempt, to make the next one wait, or to say something other than the default. Whatever it returns is shown above the form, so keep it vague: a message that says the username exists tells an attacker so too.

may_read_sign_ins async

may_read_sign_ins(
    request: Request, *, reads_everything: bool
) -> bool

Whether this person sees who signed in, on the Activity page.

Signing in belongs to no model, so no view's permissions decide it. By default only someone who may read the history of every model sees it, which reads_everything says. Override it to let in, say, an auditor who reads less.

sign_out async

sign_out(request: Request) -> None

Forget the user.

adminsite.auth.PasswordAuth

PasswordAuth(users: Mapping[str, str])

Bases: AuthProvider

Signs in against a fixed set of usernames and password hashes.

from adminsite.auth import PasswordAuth, hash_password

PasswordAuth({"nima": hash_password("letmein")})

Passwords have to be hashed with hash_password, so a plain one never ends up in your settings or your repository. This suits a small internal tool. Anything larger should subclass AuthProvider and check its own user table.

verify async

verify(username: str, password: str) -> Any | None

Check the password against the stored hash.

adminsite.auth.hash_password

hash_password(
    password: str, *, iterations: int = ITERATIONS
) -> str

Hash a password for storing.

from adminsite.auth import hash_password

hash_password("letmein")

Keep the result, not the password. The format is pbkdf2_sha256$iterations$salt$hash.

Audit log

adminsite.audit.AuditLog

AuditLog(
    source: Database | SessionSource | str = DEFAULT_URL,
    *,
    create_table: bool | None = None,
)

Bases: Store

Where the admin writes down who changed what.

By default the entries go to a SQLite file of their own, which needs no setup. Pass your engine to keep them in your own database instead; the table is then adminsite_audit_log on audit_metadata, which you add to your migrations.

Entries are written after the change they describe has committed, so a change that was rolled back never shows up in the history.

find async

find(query: AuditQuery, *, limit: int) -> list[AuditEntry]

The entries that match, newest first, at most limit of them.

history async

history(
    view: str, record_key: str, *, limit: int = 100
) -> list[AuditEntry]

What happened to one record, newest first.

recent async

recent(
    *,
    view: str | None = None,
    views: Sequence[str] | None = None,
    limit: int = 100,
) -> list[AuditEntry]

The latest entries, across the admin, for some views or for one.

record async

record(entries: Sequence[AuditEntry]) -> None

Write entries down, a batch at a time.

adminsite.audit.AuditStore

Bases: Protocol

Where the audit log is written and read back.

AuditLog is the one that ships. Write your own to keep the log in a table of your application, or to send it on elsewhere; the History tab and the Activity page read it through find either way.

record async

record(entries: Sequence[AuditEntry]) -> None

Keep these entries.

find async

find(query: AuditQuery, *, limit: int) -> list[AuditEntry]

The entries that match, newest first, at most limit of them.

Newest first means by occurred_at, then by id, both descending, which is also the order older_than pages through.

adminsite.audit.AuditQuery dataclass

AuditQuery(
    views: Sequence[str] | None = None,
    view: str | None = None,
    record_key: str | None = None,
    user: str | None = None,
    events: Sequence[AuditEvent] = (),
    since: datetime | None = None,
    until: datetime | None = None,
    older_than: tuple[datetime, int] | None = None,
)

Which entries to find. Every field narrows the result; empty means all.

  • views: only entries of these views, such as the ones a person may read. An empty string among them stands for entries of no view, such as signing in.
  • view and record_key: one view, or one record of it.
  • user: one person, matched against user_key and against user.
  • events: only these kinds of entry.
  • since and until: from since, up to but not including until.
  • older_than: the (occurred_at, id) of the last entry already shown, to find the next page after it.

adminsite.audit.AuditEntry dataclass

AuditEntry(
    view: str,
    record_key: str,
    event: AuditEvent,
    changes: Mapping[str, Change] = dict(),
    record_title: str | None = None,
    action: str | None = None,
    batch: str | None = None,
    user: str | None = None,
    message: str | None = None,
    occurred_at: datetime = (
        lambda: datetime.now(UTC).replace(tzinfo=None)
    )(),
    id: int | None = None,
    user_key: str | None = None,
    ip: str | None = None,
    user_agent: str | None = None,
    error: str | None = None,
    inputs: Mapping[str, Any] = dict(),
)

One thing that happened in the admin, who did it, and from where.

user is the name the admin shows for the person, and user_key what AuthProvider.identity returns for them, which does not change when the name does. ip and user_agent say where the request came from. error holds why something failed; it is empty when it worked. inputs holds the values an action was run with, secrets masked.

succeeded property

succeeded: bool

Whether what the entry describes worked.

as_row

as_row() -> dict[str, Any]

The entry as a row for the audit table.

from_row classmethod

from_row(row: Mapping[Any, Any]) -> AuditEntry

Read an entry back from a row of the audit table.

Errors

adminsite.RefusedError

RefusedError(message: str, *, field: str = '')

Bases: AdminSiteError

Raise this from a hook to refuse a save or a delete.

The message is shown to the user on the form or above the list, and the transaction is rolled back. Any other exception is a fault, and is reported as one.

Name a field and the message appears next to that input instead of above the form:

raise RefusedError("Keep this above the check delay.", field="delay")

adminsite.PermissionDeniedError

PermissionDeniedError(action: str, subject: str = '')

Bases: AdminSiteError

Raised when the current user may not do this.

adminsite.RecordNotFoundError

RecordNotFoundError(model: type[object], key: object)

Bases: AdminSiteError

Raised when a key does not match any record.