Metadata-Version: 2.4
Name: litedmin
Version: 0.4.1
Summary: Modern admin interface inspired by Django Admin with React Material UI frontend
Author-email: Mikhail Logunov <milog7890@gmail.com>
License: MIT
Project-URL: Homepage, https://github.com/ggb0yyyyyyy/litedmin
Project-URL: Repository, https://github.com/ggb0yyyyyyy/litedmin
Project-URL: Issues, https://github.com/ggb0yyyyyyy/litedmin/issues
Project-URL: Documentation, https://github.com/ggb0yyyyyyy/litedmin#readme
Keywords: admin,interface,django,react,material-ui,litestar,fastapi,sqlalchemy,tortoise
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.10
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: msgspec>=0.18.0
Requires-Dist: bcrypt>=4.0.0
Requires-Dist: starlette>=0.36.0
Requires-Dist: python-multipart>=0.0.6
Provides-Extra: tortoise
Requires-Dist: tortoise-orm>=0.20.0; extra == "tortoise"
Requires-Dist: aiosqlite>=0.16.0; extra == "tortoise"
Provides-Extra: sqlalchemy
Requires-Dist: sqlalchemy[asyncio]>=2.0.0; extra == "sqlalchemy"
Requires-Dist: aiosqlite>=0.16.0; extra == "sqlalchemy"
Requires-Dist: greenlet>=3.0.0; extra == "sqlalchemy"
Provides-Extra: litestar
Requires-Dist: litestar>=2.0.0; extra == "litestar"
Requires-Dist: uvicorn>=0.24.0; extra == "litestar"
Requires-Dist: httpx>=0.24.0; extra == "litestar"
Provides-Extra: fastapi
Requires-Dist: fastapi>=0.104.0; extra == "fastapi"
Requires-Dist: uvicorn>=0.24.0; extra == "fastapi"
Requires-Dist: python-multipart>=0.0.6; extra == "fastapi"
Requires-Dist: dishka>=1.2.0; extra == "fastapi"
Requires-Dist: httpx>=0.24.0; extra == "fastapi"
Provides-Extra: dev
Requires-Dist: pytest>=7.0.0; extra == "dev"
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
Requires-Dist: black>=23.0.0; extra == "dev"
Requires-Dist: mypy>=1.0.0; extra == "dev"
Requires-Dist: httpx>=0.24.0; extra == "dev"
Provides-Extra: all
Requires-Dist: litedmin[dev,fastapi,litestar,sqlalchemy,tortoise]; extra == "all"
Dynamic: license-file

# LiteAdmin

[![PyPI](https://img.shields.io/pypi/v/litedmin.svg)](https://pypi.org/project/litedmin/)
[![Python](https://img.shields.io/pypi/pyversions/litedmin.svg)](https://pypi.org/project/litedmin/)
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)

Django Admin–style admin panel for **Litestar** and **FastAPI**, with **Tortoise ORM** and **SQLAlchemy**.

React UI ships in `litedmin/static/` — **no npm** needed for normal use.

## Install

```bash
pip install litedmin
pip install "litedmin[fastapi,sqlalchemy]"   # FastAPI + SQLAlchemy
pip install "litedmin[litestar,tortoise]"    # Litestar + Tortoise
pip install "litedmin[all]"                  # both ORMs + frameworks + dev tools
```

Core symbols (`AdminConfig`, `OrmSettings`, `register`, `ModelAdmin`) import without ORM/framework extras.
ORM and web stacks load only when you import their modules (or touch lazy attributes on `litedmin`).

### From source (examples / development)

```bash
git clone https://github.com/ggb0yyyyyyy/litedmin.git
cd litedmin
pip install -e ".[litestar,tortoise]"      # examples/tortoise (port 8000)
pip install -e ".[litestar,sqlalchemy]"    # examples/sqlalchemy (port 8011)
pip install -e ".[fastapi,tortoise]"
pip install -e ".[all]"
```

## Repository layout

```
liteDmin/
  litedmin/                 installable package
    backend/
      core/                 AdminSite, config, OrmSettings, decorators
      admin/                ModelAdmin, changelist, formsets, permissions
      api/                  AdminAPI / endpoints
      auth/                 session, backends, UserAdminMixin, system models
      orm/                  Tortoise / SQLAlchemy backends + repositories
      integration/          Litestar & FastAPI Admin mounts, DI helpers
      dto/ storage/
    static/                 bundled React UI
  frontend/                 React sources (rebuild → litedmin/static/)
  examples/                 Litestar + Tortoise / SQLAlchemy demos
  tests/
```

## Quick start (Litestar + Tortoise)

Superuser is **not** created on app startup. Create it once via CLI, then run the example:

```bash
# optional credentials for CLI
export ADMIN_USERNAME=admin
export ADMIN_PASSWORD=admin

py -3.11 -m litedmin createsuperuser --settings examples.tortoise.settings:orm_settings
py -3.11 -m uvicorn examples.tortoise.app:app --host 0.0.0.0 --port 8000
```

Open http://localhost:8000/admin

SQLAlchemy example: same flow with `examples.sqlalchemy.settings:orm_settings` and port **8011**.
Details: [examples/README.md](examples/README.md), [examples/CREDENTIALS.md](examples/CREDENTIALS.md).

### Minimal wiring

```python
from litestar import Litestar, get

# Shared Starlette Admin ASGI app; Litestar/FastAPI only provide mount helpers.
from litedmin.backend.integration.litestar import Admin
from litedmin.backend.integration import init_tortoise_with_litedmin, close_tortoise
# FastAPI: from litedmin.backend.integration.fastapi import Admin
#      or: from litedmin import Admin
# Then: app.mount(admin.prefix, admin.as_asgi_app())

from examples.tortoise.admin_config import ExampleAdminConfig
from examples.tortoise.settings import AUTO_DISCOVER_MODULES, orm_settings

admin = Admin(
    prefix='/admin',
    config=ExampleAdminConfig(),          # AdminConfig: theme / UI / auth_user_model
    auto_discover_modules=AUTO_DISCOVER_MODULES,
)

async def on_startup():
    await init_tortoise_with_litedmin(orm_settings)  # OrmSettings: ORM + DB
    await admin.ensure_started()  # validate admin + sync permissions (no user seed)

async def on_shutdown():
    await close_tortoise()

@get('/')
async def root() -> dict:
    return {'admin': '/admin'}

app = Litestar(
    route_handlers=[root, admin.as_mount()],
    on_startup=[on_startup],
    on_shutdown=[on_shutdown],
)
```

### File / rich-text media storage

Pass any `StorageBackend` (`save` / `delete` / `url`) into `Admin(storage=...)`. Rich editor uploads go to `POST /admin/media/upload` and embed the returned `url` in HTML.

Examples use `LocalFileStorage` + `/media` static route — see [examples/README.md](examples/README.md). Swap in S3/MinIO by implementing the same protocol in your app.

SQLAlchemy apps also call `admin.configure_sqlalchemy(...)` after `init_sqlalchemy_with_litedmin_async` — see `examples/sqlalchemy/app.py`.

## ModelAdmin

- Prefer **`TortoiseModelAdmin`** / **`SQLAlchemyModelAdmin`** (repository save/delete).
- Register with `@register` / `AdminSite.register` — **`admin_site` is always injected**; do not construct ModelAdmin without a site.
- Permission / persist hooks are **`async`** (`has_*`, `get_permissions`, `get_list_scope_filters`, `save_model`, `delete_model`, `process_extra_fields`). Sync getters (`get_list_display`, …) stay sync.
- Users with password field: **`UserAdminMixin`**.
- Long readonly textareas: pass options on the widget itself (SPA preview + “Show full text” dialog):

```python
from litedmin import Textarea

class AnalysisAdmin(SQLAlchemyModelAdmin):
    widgets = {
        'error': Textarea(collapse_chars=280),
    }
```

### Field references (SQLAlchemy)

Settings accept plain names (`'title'`, `'author__username'`) for any ORM. For SQLAlchemy you can also pass the model's own attributes (and `Link(...)` for relation paths), so a type checker catches a misspelt field:

```python
from litedmin import Link, action, display, register
from litedmin.backend.orm.sqlalchemy import SQLAlchemyModelAdmin

@register(Post)
class PostAdmin(SQLAlchemyModelAdmin):
    list_display = [Post.id, Post.title, Link(Post.author, User.username), 'title_upper']
    search_fields = [Post.title, Link(Post.author, User.email)]
    ordering = [Post.created_at.desc()]
    list_filter = [Post.published, Post.status]
```

Tortoise models have no field attributes on the class — keep using strings there (`'title'`, `'author__username'`). Computed `@display` columns are always strings.

### Tortoise example

```python
from litedmin import action, register, display
from litedmin.backend.orm.tortoise import TortoiseModelAdmin

@action(description='Publish selected', row=True)
async def make_published(modeladmin, request, objects):
    # ``objects`` is a list of instances (not an ORM queryset)
    ...
    # Optional: return a status string for the SPA toast (e.g. background job accepted)
    # return 'Export queued'

@action(description='Refresh cache', toolbar=True, bulk=False, row=False)
async def refresh_cache(modeladmin, request, objects):
    # Page toolbar button (no selection); same POST /{model}/action with ids=[]
    ...
    return 'Refresh queued'

@register(Post)
class PostAdmin(TortoiseModelAdmin):
    list_display = ['title', 'title_upper']
    actions = [make_published, refresh_cache]  # each entry must use @action

    @display(label='Upper title')
    def title_upper(self, obj):
        return obj.title.upper()

    async def has_export_permission(self, request=None, obj=None):
        return True
```

List actions require **`@action`** (Django `@admin.action` style). Use `row=True` for per-row buttons; `bulk=False` to hide from the bulk dropdown; `toolbar=True` for page-level tools above the list (always empty selection). Signature is always `(modeladmin, request, objects)`.

### Custom pages (`@admin_page`)

For forms with arbitrary input (keys not yet in the table, etc.) — schema-driven pages bound to a model:

```python
from litedmin import admin_page, register
from litedmin.backend.orm.sqlalchemy import SQLAlchemyModelAdmin

@admin_page(
    description='Analyze by keys',
    fields=[
        {
            'name': 'keys',
            'label': 'Issue keys',
            'widget_type': 'textarea',
            'required': True,
        },
    ],
)
async def analyze_by_keys(modeladmin, request, data: dict):
    # data == {'keys': 'PRTO-1, PRTO-2'}
    return 'Queued: 2'  # toast, same as @action

@register(AnalysisResult)
class AnalysisResultAdmin(SQLAlchemyModelAdmin):
    pages = [analyze_by_keys]  # each entry must use @admin_page
```

SPA route: `/{model}/pages/{page}`. API: `GET …/pages/{page}/schema`, `POST …/pages/{page}` with `{ "data": { … } }`. Default permission: `has_change_permission`.

## Auth

- Default: **session cookie** login (`SessionAuthBackend` + `SessionAuth`).
- SSO (Keycloak gateway handoff): `GatewayWebAuthBackend` — see `examples/tortoise/app_gateway_web.py` and [examples/README.md](examples/README.md).

## CLI

```bash
litedmin createsuperuser --settings examples.tortoise.settings:orm_settings
# or
python -m litedmin createsuperuser --settings module.path:orm_settings
```

`--username` / `--password` / `--email`, or `ADMIN_USERNAME` / `ADMIN_PASSWORD` / `ADMIN_EMAIL`.

## Config split

| Object | Role |
|--------|------|
| **`AdminConfig`** | Theme, UI, sidebar groups, optional `auth_user_model` |
| **`OrmSettings`** | `orm_type`, database URL, app modules for ORM init |

## Features

- `@register` / `AdminSite.register` ModelAdmin API
- SQLAlchemy field attributes / `Link(...)` in ModelAdmin settings (strings for Tortoise)
- `@display` computed list columns
- `@action` list actions (bulk dropdown + optional `row=True` / `toolbar=True` buttons)
- `@admin_page` schema-driven custom forms on a model (changelist page buttons)
- CSV/JSON export (`has_export_permission`)
- Django-like permissions, groups, LogEntry audit trail
- `list_editable`, `date_hierarchy`, bulk actions, inlines
- Row-level scope via `get_list_scope_filters` + object-level `has_*`
- Double-submit CSRF cookie for session auth (`AdminConfig.csrf_protect`)
- `StorageBackend` protocol for file uploads

## Typing

Package is typed; project mypy config is in `pyproject.toml` (`[tool.mypy]`).

```bash
pip install -e ".[dev]"
python -m mypy litedmin
```

## Change the React UI

```bash
pip install -e ".[all]"
# or: pip install -r requirements-dev.txt
bash scripts/build_frontend.sh
git add litedmin/static/
```

## License

MIT
