feat: add gradient entity with CRUD API and storage

Reusable gradient definitions with built-in presets (rainbow, sunset,
ocean, etc.) and user-created gradients. Includes model, JSON store,
Pydantic schemas, REST routes (list/create/update/clone/delete), and
backup/restore integration.
This commit is contained in:
2026-03-24 13:58:04 +03:00
parent 2c3f08344c
commit c26aec916e
8 changed files with 476 additions and 0 deletions
@@ -0,0 +1,83 @@
"""Gradient data model.
A Gradient defines a reusable color gradient as a list of color stops.
Gradients are referenced by ID from effect, gradient, and audio color
strip sources. Eight built-in gradients are seeded on first run.
"""
from dataclasses import dataclass, field
from datetime import datetime
from typing import List, Optional
@dataclass
class Gradient:
"""Persistent gradient definition with color stops."""
id: str
name: str
stops: list # [{"position": float, "color": [R, G, B]}, ...]
is_builtin: bool
created_at: datetime
updated_at: datetime
description: Optional[str] = None
tags: List[str] = field(default_factory=list)
def to_dict(self) -> dict:
return {
"id": self.id,
"name": self.name,
"stops": self.stops,
"is_builtin": self.is_builtin,
"description": self.description,
"tags": self.tags,
"created_at": self.created_at.isoformat(),
"updated_at": self.updated_at.isoformat(),
}
@staticmethod
def from_dict(data: dict) -> "Gradient":
return Gradient(
id=data["id"],
name=data["name"],
stops=data.get("stops", []),
is_builtin=data.get("is_builtin", False),
description=data.get("description"),
tags=data.get("tags", []),
created_at=datetime.fromisoformat(data["created_at"]),
updated_at=datetime.fromisoformat(data["updated_at"]),
)
@classmethod
def create_from_kwargs(
cls,
*,
id: str,
name: str,
stops: list,
is_builtin: bool = False,
created_at: datetime,
updated_at: datetime,
description: Optional[str] = None,
tags: Optional[List[str]] = None,
) -> "Gradient":
return cls(
id=id,
name=name,
stops=stops,
is_builtin=is_builtin,
created_at=created_at,
updated_at=updated_at,
description=description,
tags=tags or [],
)
def apply_update(self, **kwargs) -> None:
if kwargs.get("name") is not None:
self.name = kwargs["name"]
if kwargs.get("stops") is not None:
self.stops = kwargs["stops"]
if kwargs.get("description") is not None:
self.description = kwargs["description"]
if kwargs.get("tags") is not None:
self.tags = kwargs["tags"]