# version: v1 """Chart-template REST endpoints. A "chart template" is a named full-chart-config snapshot the operator can save and re-apply to any chart ("XAUUSD swing", "XAUUSD scalp", ...). Distinct from /api/chart-presets, which holds chart-STYLE-only fragments. Wire into data_server.py: from chart_templates_routes import create_chart_templates_router app.include_router(create_chart_templates_router()) Endpoints: GET /api/chart-templates — returns dict of all named templates {name: {sourceTicker, config, savedAt}} PUT /api/chart-templates/{name} — body is {sourceTicker, config, savedAt}; creates or replaces DELETE /api/chart-templates/{name} — removes the named template (idempotent) """ import logging from typing import Any from urllib.parse import unquote from fastapi import APIRouter, HTTPException from pydantic import BaseModel import config_manager log = logging.getLogger("mk.chart_templates_routes") # Permissive body model: a template carries a sourceTicker, a config dict # (the full chart-config bundle), and a savedAt timestamp. `extra = "allow"` # keeps the API forward-compatible if the shape grows. class ChartTemplateIn(BaseModel): sourceTicker: str | None = None config: dict | None = None savedAt: str | None = None class Config: extra = "allow" _RESERVED_NAMES = {"defaults", "default"} _MAX_NAME_LEN = 60 def _validate_name(name: str) -> None: if not name or not name.strip(): raise HTTPException(400, detail="Template name cannot be empty") if len(name) > _MAX_NAME_LEN: raise HTTPException(400, detail=f"Template name exceeds {_MAX_NAME_LEN} characters") if name.strip().lower() in _RESERVED_NAMES: raise HTTPException(400, detail=f"'{name}' is a reserved name") def create_chart_templates_router() -> APIRouter: router = APIRouter(prefix="/api/chart-templates", tags=["chart-templates"]) @router.get("") async def list_templates() -> dict[str, Any]: return config_manager.get_chart_templates() @router.put("/{name}") async def upsert_template(name: str, body: ChartTemplateIn) -> dict[str, Any]: decoded = unquote(name) _validate_name(decoded) try: config_manager.save_chart_template(decoded, body.model_dump(exclude_none=False)) except Exception as e: log.exception("save_chart_template failed") raise HTTPException(500, detail=f"Save failed: {e}") from e return config_manager.get_chart_templates() @router.delete("/{name}") async def delete_template(name: str) -> dict[str, Any]: decoded = unquote(name) try: config_manager.delete_chart_template(decoded) except Exception as e: log.exception("delete_chart_template failed") raise HTTPException(500, detail=f"Delete failed: {e}") from e return config_manager.get_chart_templates() return router