16 KiB
16 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 01-foundation | 01 | execute | 1 |
|
true |
|
|
Purpose: Establishes every backend module, data model, and persistence mechanism that all future phases depend on. The models defined here are the contracts for the entire system.
Output: A runnable python -m lightsync that starts FastAPI with route stubs, device registry, and show store.
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/01-foundation/01-CONTEXT.md @.planning/phases/01-foundation/01-RESEARCH.md Task 1: Create package structure, Pydantic models, and pyproject.toml pyproject.toml .env lightsync/__init__.py lightsync/models/__init__.py lightsync/models/device.py lightsync/models/show.py lightsync/devices/__init__.py lightsync/devices/base.py lightsync/devices/sk6812.py lightsync/devices/ws2801.py .planning/phases/01-foundation/01-RESEARCH.md .planning/phases/01-foundation/01-CONTEXT.md Create pyproject.toml (per D-05) with: - [build-system] requires = ["hatchling"], build-backend = "hatchling.build" - [project] name = "lightsync", version = "0.1.0", requires-python = ">=3.11" - dependencies: fastapi>=0.115.0, uvicorn[standard]>=0.34.0, pydantic>=2.0.0, aiofiles>=24.0.0, python-dotenv>=1.0.0, structlog>=24.0.0 - [project.optional-dependencies] dev = ["httpx>=0.27.0"]Create .env with HOST=0.0.0.0 and PORT=8000.
Create lightsync/__init__.py with `__version__ = "0.1.0"`.
Create lightsync/models/__init__.py that re-exports DeviceConfig, ShowModel.
Create lightsync/models/device.py (per Research Pattern 2):
- StripType = Literal["sk6812", "ws2801", "generic"]
- class DeviceConfig(BaseModel): id: UUID = Field(default_factory=uuid4), name: str, strip_type: StripType, led_count: int = Field(gt=0, le=1000), ip: str, port: int = Field(ge=1, le=65535), enabled: bool = True
- model_config = {"populate_by_name": True}
Create lightsync/models/show.py (per Research Pattern 2):
- class AudioRef(BaseModel): source_type: Literal["file", "youtube"] = "file", path: str | None = None, yt_url: str | None = None, duration_seconds: float | None = None
- class CueModel(BaseModel): id: UUID = Field(default_factory=uuid4), timestamp: float, mode: Literal["animation", "frame_sequence"] = "animation", animation: str | None = None, params: dict[str, Any] = Field(default_factory=dict)
- class TrackModel(BaseModel): device_id: UUID, cues: list[CueModel] = Field(default_factory=list)
- class AnalysisBlock(BaseModel): analysed_at: datetime | None = None, tempo_bpm: float | None = None, beat_times: list[float] = Field(default_factory=list), onset_times: list[float] = Field(default_factory=list)
- class ShowModel(BaseModel): schema_version: int = 1, id: UUID = Field(default_factory=uuid4), name: str, created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)), updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)), audio: AudioRef = Field(default_factory=AudioRef), devices: list[DeviceConfig] = Field(default_factory=list), analysis: AnalysisBlock = Field(default_factory=AnalysisBlock), tracks: list[TrackModel] = Field(default_factory=list), ai_sequences: list[Any] = Field(default_factory=list)
Create lightsync/devices/__init__.py (empty).
Create lightsync/devices/base.py (per Research Pattern 3):
- class BaseDevice(ABC) with __init__(self, config: DeviceConfig), properties id and led_count
- @abstractmethod encode_frame(self, pixels: list[tuple]) -> bytes
- @abstractmethod encode_animation_cmd(self, animation: str, params: dict) -> bytes
- @property @abstractmethod bytes_per_pixel(self) -> int
Create lightsync/devices/sk6812.py:
- class SK6812Device(BaseDevice) with bytes_per_pixel = 4
- encode_frame: iterate pixels, for each (r, g, b, w) append 4 bytes to bytearray; return bytes
- encode_animation_cmd: stub returning b"" (Phase 3 implements real encoding)
Create lightsync/devices/ws2801.py:
- class WS2801Device(BaseDevice) with bytes_per_pixel = 3
- encode_frame: iterate pixels, for each (r, g, b) append 3 bytes to bytearray; return bytes
- encode_animation_cmd: stub returning b"" (Phase 3 implements real encoding)
cd /home/claude/led2 && python -c "from lightsync.models.device import DeviceConfig; d = DeviceConfig(name='test', strip_type='sk6812', led_count=60, ip='192.168.1.10', port=21324); print(d.model_dump_json())" && python -c "from lightsync.models.show import ShowModel; s = ShowModel(name='test'); print(s.schema_version); assert s.schema_version == 1; print(s.model_dump_json()[:100])" && python -c "from lightsync.devices.sk6812 import SK6812Device; from lightsync.models.device import DeviceConfig; d = SK6812Device(DeviceConfig(name='t', strip_type='sk6812', led_count=1, ip='0', port=1)); assert d.bytes_per_pixel == 4; print('OK')"
- pyproject.toml contains `name = "lightsync"` and `fastapi>=0.115.0` and `aiofiles>=24.0.0`
- lightsync/models/device.py contains `class DeviceConfig(BaseModel)` and `StripType = Literal["sk6812", "ws2801", "generic"]`
- lightsync/models/show.py contains `schema_version: int = 1` and `class ShowModel(BaseModel)` and `class AudioRef(BaseModel)` and `class TrackModel(BaseModel)` and `class CueModel(BaseModel)` and `class AnalysisBlock(BaseModel)`
- lightsync/devices/base.py contains `class BaseDevice(ABC)` and `def encode_frame` and `def encode_animation_cmd` and `def bytes_per_pixel`
- lightsync/devices/sk6812.py contains `class SK6812Device(BaseDevice)` and `bytes_per_pixel` returning 4
- lightsync/devices/ws2801.py contains `class WS2801Device(BaseDevice)` and `bytes_per_pixel` returning 3
- `python -c "from lightsync.models.show import ShowModel; s = ShowModel(name='x'); assert s.schema_version == 1"` exits 0
All Pydantic models serialize/deserialize correctly. BaseDevice ABC exists with two concrete implementations. pyproject.toml lists all Phase 1 dependencies.
Task 2: Create device registry, show store, FastAPI app with lifespan, API route stubs, and WebSocket stub
lightsync/devices/registry.py
lightsync/store/__init__.py
lightsync/store/show_store.py
lightsync/api/__init__.py
lightsync/api/devices.py
lightsync/api/shows.py
lightsync/api/ws.py
lightsync/main.py
lightsync/__main__.py
.planning/phases/01-foundation/01-RESEARCH.md
lightsync/models/device.py
lightsync/models/show.py
lightsync/devices/base.py
Create lightsync/devices/registry.py (per Research Pattern 3):
- _DEVICE_CLASSES: dict[str, type[BaseDevice]] = {"sk6812": SK6812Device, "ws2801": WS2801Device}
- class DeviceRegistry with __init__(self, path: Path), _devices: dict[str, DeviceConfig]
- async load(): read JSON from self._path, parse each entry via DeviceConfig.model_validate
- async save(): serialize all devices via model_dump(mode="json"), write JSON with indent=2
- add(config: DeviceConfig), remove(device_id: str) -> bool, get(device_id: str) -> DeviceConfig | None, list_all() -> list[DeviceConfig]
- instantiate(device_id: str) -> BaseDevice | None: lookup config, lookup class in _DEVICE_CLASSES, return instance
Create lightsync/store/__init__.py (empty).
Create lightsync/store/show_store.py (per Research Pattern 4):
- class ShowStore with __init__(self, shows_dir: Path)
- ensure_dir(): self._dir.mkdir(parents=True, exist_ok=True)
- _path_for(show_id: str) -> Path: return self._dir / f"{show_id}.json"
- async save(show: ShowModel): write show.model_dump_json(indent=2) to _path_for(str(show.id))
- async load(show_id: str) -> ShowModel | None: read file, return ShowModel.model_validate_json(content)
- list_ids() -> list[str]: return [p.stem for p in self._dir.glob("*.json")]
Create lightsync/api/__init__.py (empty).
Create lightsync/api/devices.py (per Research Pattern 5):
- router = APIRouter(tags=["devices"])
- GET / -> list[DeviceConfig]: return state.registry.list_all()
- POST / status_code=201 -> DeviceConfig: registry.add(config), await registry.save(), return config
- DELETE /{device_id} status_code=204: registry.remove or raise HTTPException(404)
- Import registry via `import lightsync.main as state` and access `state.registry`
Create lightsync/api/shows.py:
- router = APIRouter(tags=["shows"])
- GET / -> list of show summaries: iterate show_store.list_ids(), load each, return list of {id, name, created_at}
- POST / status_code=201: accept ShowModel body, save via show_store, return show
- GET /{show_id}: load from show_store or raise 404
- Import show_store via `import lightsync.main as state` and access `state.show_store`
Create lightsync/api/ws.py (per Research Pattern 6):
- class ConnectionManager with active_connections list, connect/disconnect/broadcast methods
- module-level `manager = ConnectionManager()`
- @router.websocket("/ws"): accept, loop receive_text, echo back {"type": "ack", "echo": msg}
- Handle WebSocketDisconnect with manager.disconnect
Create lightsync/main.py (per Research Pattern 1):
- Module-level: registry: DeviceRegistry | None = None, show_store: ShowStore | None = None
- @asynccontextmanager async def lifespan(app): initialize registry from Path("devices.json"), await registry.load(), initialize show_store from Path("shows"), show_store.ensure_dir(), yield, await registry.save()
- def create_app() -> FastAPI: create app with title="LightSync" and lifespan, include_router for shows (prefix="/api/shows"), devices (prefix="/api/devices"), ws; mount StaticFiles LAST at "/" with directory=Path(__file__).parent / "frontend" and html=True
- app = create_app()
Create lightsync/__main__.py (per Research Pattern 1):
- if __name__ == "__main__": load_dotenv(), uvicorn.run("lightsync.main:app", host from env default "0.0.0.0", port from env default 8000, reload=False)
cd /home/claude/led2 && pip install -e ".[dev]" 2>&1 | tail -3 && python -c "from lightsync.devices.registry import DeviceRegistry; from lightsync.store.show_store import ShowStore; print('imports OK')" && timeout 5 python -m lightsync 2>&1 || true
- lightsync/devices/registry.py contains `_DEVICE_CLASSES` and `class DeviceRegistry` and `async def load` and `async def save` and `def instantiate`
- lightsync/store/show_store.py contains `class ShowStore` and `model_dump_json` and `model_validate_json` and `def list_ids`
- lightsync/api/devices.py contains `router = APIRouter` and `async def list_devices` and `async def add_device` and `async def remove_device`
- lightsync/api/shows.py contains `router = APIRouter` and `/api/shows` routes for GET and POST
- lightsync/api/ws.py contains `class ConnectionManager` and `@router.websocket("/ws")`
- lightsync/main.py contains `async def lifespan` and `StaticFiles` and `include_router` and routes registered BEFORE StaticFiles mount
- lightsync/__main__.py contains `if __name__ == "__main__"` and `uvicorn.run`
- `pip install -e .` completes without error
- `python -m lightsync` starts and listens on port 8000 (may fail on missing frontend dir — that is OK, plan 01-02 creates it)
FastAPI app starts via `python -m lightsync`. Device registry persists to devices.json. Show store saves/loads JSON files. All API route stubs respond. WebSocket stub echoes messages.
1. `pip install -e ".[dev]"` succeeds
2. `python -c "from lightsync.models.show import ShowModel; s = ShowModel(name='test'); assert s.schema_version == 1"` passes
3. `python -c "from lightsync.devices.registry import DeviceRegistry; print('OK')"` passes
4. `python -m lightsync` starts without import errors (may warn about missing frontend/ directory)
<success_criteria>
- All Python modules under lightsync/ import without error
- Pydantic models serialize to and deserialize from JSON correctly
- DeviceRegistry round-trips through devices.json
- ShowStore round-trips through shows/ directory
- FastAPI app starts and registers all route prefixes
- Adding a new strip type requires only a new file in devices/ and one dict entry </success_criteria>