"""Gradient routes: CRUD for reusable gradient definitions.""" from fastapi import APIRouter, Depends, HTTPException from wled_controller.api.auth import AuthRequired from wled_controller.api.dependencies import ( fire_entity_event, get_color_strip_store, get_gradient_store, ) from wled_controller.api.schemas.gradients import ( GradientCreate, GradientListResponse, GradientResponse, GradientUpdate, ) from wled_controller.storage.gradient import Gradient from wled_controller.storage.gradient_store import GradientStore from wled_controller.storage.color_strip_store import ColorStripStore from wled_controller.storage.base_store import EntityNotFoundError from wled_controller.utils import get_logger logger = get_logger(__name__) router = APIRouter() def _to_response(gradient: Gradient) -> GradientResponse: return GradientResponse( id=gradient.id, name=gradient.name, stops=[{"position": s["position"], "color": s["color"]} for s in gradient.stops], is_builtin=gradient.is_builtin, description=gradient.description, tags=gradient.tags, created_at=gradient.created_at, updated_at=gradient.updated_at, ) @router.get("/api/v1/gradients", response_model=GradientListResponse, tags=["Gradients"]) async def list_gradients( _auth: AuthRequired, store: GradientStore = Depends(get_gradient_store), ): """List all gradients (built-in + user-created).""" gradients = store.get_all_gradients() return GradientListResponse( gradients=[_to_response(g) for g in gradients], count=len(gradients), ) @router.post("/api/v1/gradients", response_model=GradientResponse, status_code=201, tags=["Gradients"]) async def create_gradient( data: GradientCreate, _auth: AuthRequired, store: GradientStore = Depends(get_gradient_store), ): """Create a new user-defined gradient.""" try: gradient = store.create_gradient( name=data.name, stops=[s.model_dump() for s in data.stops], description=data.description, tags=data.tags, ) fire_entity_event("gradient", "created", gradient.id) return _to_response(gradient) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) @router.get("/api/v1/gradients/{gradient_id}", response_model=GradientResponse, tags=["Gradients"]) async def get_gradient( gradient_id: str, _auth: AuthRequired, store: GradientStore = Depends(get_gradient_store), ): """Get a gradient by ID.""" try: gradient = store.get_gradient(gradient_id) return _to_response(gradient) except (ValueError, EntityNotFoundError) as e: raise HTTPException(status_code=404, detail=str(e)) @router.put("/api/v1/gradients/{gradient_id}", response_model=GradientResponse, tags=["Gradients"]) async def update_gradient( gradient_id: str, data: GradientUpdate, _auth: AuthRequired, store: GradientStore = Depends(get_gradient_store), ): """Update a gradient (built-in gradients are read-only).""" try: stops = [s.model_dump() for s in data.stops] if data.stops is not None else None gradient = store.update_gradient( gradient_id=gradient_id, name=data.name, stops=stops, description=data.description, tags=data.tags, ) fire_entity_event("gradient", "updated", gradient_id) return _to_response(gradient) except (ValueError, EntityNotFoundError) as e: status = 404 if "not found" in str(e).lower() else 400 raise HTTPException(status_code=status, detail=str(e)) @router.post("/api/v1/gradients/{gradient_id}/clone", response_model=GradientResponse, status_code=201, tags=["Gradients"]) async def clone_gradient( gradient_id: str, _auth: AuthRequired, store: GradientStore = Depends(get_gradient_store), ): """Clone a gradient (useful for customizing built-in gradients).""" try: original = store.get_gradient(gradient_id) clone = store.create_gradient( name=f"{original.name} (copy)", stops=original.stops, description=original.description, tags=original.tags, ) fire_entity_event("gradient", "created", clone.id) return _to_response(clone) except (ValueError, EntityNotFoundError) as e: status = 404 if "not found" in str(e).lower() else 400 raise HTTPException(status_code=status, detail=str(e)) @router.delete("/api/v1/gradients/{gradient_id}", status_code=204, tags=["Gradients"]) async def delete_gradient( gradient_id: str, _auth: AuthRequired, store: GradientStore = Depends(get_gradient_store), css_store: ColorStripStore = Depends(get_color_strip_store), ): """Delete a gradient (fails if built-in or referenced by sources).""" try: # Check references for source in css_store.get_all_sources(): if getattr(source, "gradient_id", None) == gradient_id: raise ValueError( f"Cannot delete: referenced by color strip source '{source.name}'" ) store.delete_gradient(gradient_id) fire_entity_event("gradient", "deleted", gradient_id) except (ValueError, EntityNotFoundError) as e: status = 404 if "not found" in str(e).lower() else 400 raise HTTPException(status_code=status, detail=str(e))