This commit is contained in:
2026-08-19 15:08:39 +03:00
parent 478557f3ad
commit a03c54dbe8
5148 changed files with 31553313 additions and 0 deletions

131
ui_builder/router.py Normal file
View File

@@ -0,0 +1,131 @@
from __future__ import annotations
import inspect
from collections.abc import Awaitable, Callable
from datetime import datetime
from typing import Any
from fastapi import APIRouter, Depends, HTTPException
from fastapi.responses import JSONResponse
from pydantic import BaseModel, ConfigDict, Field
from .manager import UIBuilderManager
DataProvider = Callable[[], dict[str, Any] | Awaitable[dict[str, Any]]]
class ConfigPayload(BaseModel):
model_config = ConfigDict(extra="allow")
version: int = 20
project_name: str
data_source: str
canvas: dict[str, Any] = Field(default_factory=dict)
tabs: list[dict[str, Any]] = Field(default_factory=list)
components: list[dict[str, Any]] = Field(default_factory=list)
triggers: list[dict[str, Any]] = Field(default_factory=list)
shortcut_sequences: list[dict[str, Any]] = Field(default_factory=list)
prematch_groups: list[dict[str, Any]] = Field(default_factory=list)
prematch_buttons: list[dict[str, Any]] = Field(default_factory=list)
quick_panel_selectors: list[dict[str, Any]] = Field(default_factory=list)
class BackupPayload(BaseModel):
name: str
def create_ui_builder_router(
manager: UIBuilderManager,
data_providers: dict[str, DataProvider],
*,
source_labels: dict[str, str] | None = None,
prefix: str = "/api/ui-builder",
access_dependency: Callable[..., Any] | None = None,
read_only: bool = False,
publish_manager: UIBuilderManager | None = None,
) -> APIRouter:
dependencies = [Depends(access_dependency)] if access_dependency else []
router = APIRouter(
prefix=prefix,
tags=["UI Builder"],
dependencies=dependencies,
)
source_labels = source_labels or {}
@router.get("/config")
async def get_config() -> dict[str, Any]:
return manager.load()
if not read_only:
@router.post("/config")
async def save_config(payload: ConfigPayload) -> dict[str, Any]:
saved = manager.save(payload.model_dump())
return {"ok": True, "config": saved}
@router.post("/import")
async def import_config(payload: ConfigPayload) -> dict[str, Any]:
saved = manager.import_config(payload.model_dump())
return {"ok": True, "config": saved}
@router.get("/export")
async def export_config() -> JSONResponse:
return JSONResponse(
manager.load(),
headers={
"Content-Disposition":
'attachment; filename="ui_builder_draft.json"'
},
)
@router.get("/backups")
async def list_backups() -> dict[str, Any]:
return {"items": manager.list_backups()}
@router.post("/backups/restore")
async def restore_backup(payload: BackupPayload) -> dict[str, Any]:
try:
restored = manager.restore_backup(payload.name)
except (FileNotFoundError, ValueError, OSError):
raise HTTPException(status_code=404, detail="Backup not found")
return {"ok": True, "config": restored}
if publish_manager is not None:
@router.post("/publish")
async def publish_config() -> dict[str, Any]:
published = publish_manager.save(
manager.load(),
create_backup=True,
)
return {
"ok": True,
"config": published,
"published_at": datetime.now().isoformat(timespec="seconds"),
}
@router.get("/sources")
async def list_sources() -> dict[str, Any]:
return {
"items": [
{"id": key, "label": source_labels.get(key, key)}
for key in data_providers
]
}
@router.get("/data/{source}")
async def get_data(source: str) -> dict[str, Any]:
provider = data_providers.get(source)
if provider is None:
raise HTTPException(status_code=404, detail="Unknown data source")
try:
result = provider()
if inspect.isawaitable(result):
result = await result
if not isinstance(result, dict):
raise TypeError("Data provider must return a dict")
return result
except HTTPException:
raise
except Exception as exc:
raise HTTPException(status_code=500, detail=str(exc)) from exc
return router