Metadata-Version: 2.4
Name: xladmin
Version: 0.10.0
Summary: Generic FastAPI + SQLAlchemy admin backend.
Author: Artasov
License-Expression: MIT
Project-URL: Homepage, https://github.com/Artasov/xladmin/tree/main/xladmin-backend
Project-URL: Repository, https://github.com/Artasov/xladmin
Project-URL: Issues, https://github.com/Artasov/xladmin/issues
Requires-Python: >=3.12
Description-Content-Type: text/markdown
Requires-Dist: fastapi<1.0.0,>=0.115.0
Requires-Dist: pydantic<3.0.0,>=2.9.0
Requires-Dist: sqlalchemy<3.0.0,>=2.0.0
Provides-Extra: test
Requires-Dist: aiosqlite>=0.20.0; extra == "test"
Requires-Dist: asyncpg>=0.30.0; extra == "test"
Requires-Dist: httpx>=0.28.0; extra == "test"
Requires-Dist: python-dotenv>=1.0.1; extra == "test"
Requires-Dist: pytest>=8.0.0; extra == "test"
Requires-Dist: pytest-asyncio>=1.0.0; extra == "test"
Provides-Extra: dev
Requires-Dist: aiosqlite>=0.20.0; extra == "dev"
Requires-Dist: asyncpg>=0.30.0; extra == "dev"
Requires-Dist: build>=1.2.2; extra == "dev"
Requires-Dist: httpx>=0.28.0; extra == "dev"
Requires-Dist: mypy>=1.11.0; extra == "dev"
Requires-Dist: python-dotenv>=1.0.1; extra == "dev"
Requires-Dist: pytest>=8.0.0; extra == "dev"
Requires-Dist: pytest-asyncio>=1.0.0; extra == "dev"
Requires-Dist: ruff>=0.11.0; extra == "dev"
Requires-Dist: twine>=6.1.0; extra == "dev"

<div align="center">
  <a href="./README.md">
    <img src="https://img.shields.io/badge/English-blue?style=for-the-badge" alt="English">
  </a>
  <a href="./docs/README.ru.md">
    <img src="https://img.shields.io/badge/%D0%A0%D1%83%D1%81%D1%81%D0%BA%D0%B8%D0%B9-red?style=for-the-badge" alt="Russian">
  </a>
</div>

# xladmin backend

`xladmin-backend` is the backend package published to PyPI as `xladmin`.

Important:

- package name on PyPI: `xladmin`
- Python import: `from xladmin import ...`
- monorepo: [Artasov/xladmin](https://github.com/Artasov/xladmin)

## Public API

- `AdminConfig` / `ModelConfig` / `FieldConfig`
- `ListFilterConfig`
- `BulkActionConfig` / `ObjectActionConfig`
- `ModelsBlock`
- `HttpConfig`
- `create_router(...)`

Compatibility aliases are kept:

- `Admin*` config names
- `create_admin_router(...)`

## Read-only models

Set `ModelConfig(..., read_only=True)` for records that may only be changed through application domain commands. This is a model-level write restriction, separate from `FieldConfig.read_only` and from user authentication.

The admin rejects create, PATCH, delete, bulk delete, object actions and bulk actions with HTTP 403 before invoking their write handlers. List/detail and relation reads remain available. Metadata includes `read_only`, removes writable fields and write actions, and marks fields as read-only even when explicit create/update field lists were configured.

Deleting a writable parent is also rejected if the deletion plan would delete a registered read-only child or clear its foreign key. Delete previews expose those dependencies as protected. Custom handlers registered on other writable models are trusted application code; this setting is not a database-wide authorization mechanism for arbitrary SQL issued by them.

The matching import/export extension rejects both import validation and commit, advertises no import formats, and preserves exports. Consumers must update the backend core and extension together. Frontend consumers should use model metadata to hide write controls; hiding controls alone is not the protection.

## Scoped relationship writes

`ModelConfig.query_for_list(query, session, user)` also defines the available IDs for registered relation targets. Standard create/PATCH validates scalar foreign keys, single relationships and every ID in a relationship collection against this scope, even when an unavailable object is already cached in the session. An unavailable ID produces HTTP 400 without committing the parent edit; duplicate collection IDs are deduplicated in request order. Models without a configured scope retain their ordinary relation selection semantics.

Internal mutation helpers now require the runtime `registry` and authenticated `user` as keyword arguments. Upgrade the matching import/export extension together: its validation and commit pass the same context, and imported existing targets must also be visible. Custom setters/handlers remain trusted application code. This is assignment validation, not automatic database routing or row-level security for arbitrary SQL.

## Minimal Example

```python
from xladmin import AdminConfig, HttpConfig, ModelConfig, create_router

from src.core.auth.dependencies import get_current_user
from src.core.db.session import get_db_session
from src.modules.identity.models import UserORM


config = AdminConfig(
    models=(
        ModelConfig(model=UserORM),
    ),
)

router = create_router(
    HttpConfig(
        registry=config,
        get_db_session_dependency=get_db_session,
        get_current_user_dependency=get_current_user,
        is_allowed=lambda user: bool(user.is_staff),
    ),
)
```

`ModelConfig(model=UserORM)` is enough for a basic admin. The library derives default `slug`, `title`, search fields, and ordering from the ORM model.

## Features

- list / detail / create / patch / delete endpoints
- current-user endpoint for frontend sidebar identity: `GET /xladmin/me/`
- logout endpoint for frontend logout buttons: `POST /xladmin/logout/`
- bulk actions and object actions
- relation choices and relation filters
- single and multi-select relation filters via `ListFilterConfig(..., multiple=True, input_kind="relation-multiple")`
- overview metadata and model blocks
- `query_for_list` and custom `search_query_builder`
- mode-specific form fields with `hidden_in_create` / `hidden_in_update`
- custom create defaults with `create_item_factory`
- delete preview for single and bulk delete
- RU / EN locale metadata for the frontend

## Current User And Logout

The frontend `Shell` can show the current user in the sidebar and call logout from the sidebar action.
The backend router exposes two endpoints for this:

- `GET /xladmin/me/`
- `POST /xladmin/logout/`

`/xladmin/me/` uses `get_current_user_dependency` and returns a small payload:

```json
{
  "id": 1,
  "login": "admin@example.com",
  "email": "admin@example.com",
  "name": "Admin"
}
```

The `login` value is resolved from the first available user attribute in this order:
`username`, `email`, `login`, `name`, then `id`.

By default `/xladmin/logout/` only checks that the user can access admin and returns `204`.
Real applications should pass `logout_dependency` to `HttpConfig` to clear cookies, sessions, tokens, or any other auth state.
Headers and cookies set by `logout_dependency` are preserved in the final `204` response.

```python
from fastapi import Response
from xladmin import HttpConfig, create_router


async def logout_admin_user(response: Response) -> None:
    response.delete_cookie("session")


router = create_router(
    HttpConfig(
        registry=admin_config,
        get_db_session_dependency=get_db_session,
        get_current_user_dependency=get_current_user,
        is_allowed=lambda user: bool(user.is_staff),
        logout_dependency=logout_admin_user,
    ),
)
```

If your auth system needs the current user in the logout handler, add it as a regular FastAPI dependency inside your function.

## Multi-Select Relation Filters

If one relation filter is not enough, you can expose a multi-select variant that works as an autocomplete with add/remove chips on the frontend.

```python
from xladmin import ListFilterConfig


ListFilterConfig(
    slug="role_ids",
    label="Roles",
    field_name="roles",
    input_kind="relation-multiple",
    multiple=True,
    relation_model=RoleORM,
    relation_label_field="name",
)
```

## Create And Update Fields

If a field should be visible only in one form mode, use `hidden_in_create` or `hidden_in_update`.

```python
from xladmin import FieldConfig, ModelConfig


ModelConfig(
    model=UserORM,
    fields={
        "password": FieldConfig(
            input_kind="password",
            hidden_in_update=True,
            value_setter=set_user_password,
        ),
        "new_password": FieldConfig(
            input_kind="password",
            hidden_in_create=True,
            value_getter=lambda _user: "",
            value_setter=set_user_password,
        ),
    },
)
```

If create requires hidden service fields, use `create_item_factory`.

```python
from xladmin import ModelConfig


def create_admin_user(payload, session, user):
    del payload, session, user
    return UserORM(
        date_joined=AuthBase.now(),
        secret_key=AuthBase.generate_secret_key(),
    )


ModelConfig(
    model=UserORM,
    create_fields=("username", "email", "password"),
    create_item_factory=create_admin_user,
)
```

If create needs a fully custom form and payload handler, use `create_form` + `create_handler`.

The same `FormFieldConfig` mechanism is also available for `object_actions` and `bulk_actions` via `form=...`.
If an action has no form, the frontend runs it immediately as before.

For `datetime` inputs the built-in dialog uses the MUI action bar with a localized `Today` / `Сегодня` button, which inserts the current date and time.

```python
from xladmin import FormFieldConfig, FormFieldOptionConfig, ModelConfig


async def create_proxy(session, model_config, payload, user):
    del session, model_config, user
    parsed = ProxyBase.parse_raw(f"{payload['scheme']}://{payload['proxy']}")
    return ProxyORM(
        name=parsed.name,
        scheme=parsed.scheme,
        host=parsed.host,
        port=parsed.port,
        username=parsed.username,
        password=parsed.password,
        created_at=ProxyBase.now(),
        updated_at=ProxyBase.now(),
    )


ModelConfig(
    model=ProxyORM,
    create_form=(
        FormFieldConfig(
            name="scheme",
            label="Scheme",
            input_kind="select",
            required=True,
            options=(
                FormFieldOptionConfig(value="http", label="HTTP"),
                FormFieldOptionConfig(value="socks5h", label="SOCKS5H"),
            ),
        ),
        FormFieldConfig(
            name="proxy",
            label="Proxy",
            placeholder="login:password@ip:port",
            required=True,
        ),
    ),
    create_handler=create_proxy,
)
```

## Compatibility

- `FastAPI >=0.115,<1.0`
- `Pydantic >=2.9,<3.0`
- `SQLAlchemy >=2.0,<3.0`
- `Python >=3.12`

## Development

```bash
uv sync --extra dev
uv run pytest
uv run ruff check .
uv run mypy
uv run python -m build
uv run python -m twine check dist/*
```

## Docs

- [How to use](./docs/HOW_TO_USE.en.md)
- [PyPI package](https://pypi.org/project/xladmin/)
- [xladmin-import-export backend](../xladmin-import-export/README.md)
- [Russian README](./docs/README.ru.md)
- [Monorepo README](../README.md)
