Files
DECNET/decnet/web/db/models/decky_lifecycle.py
anti f2b3393669 chore: relicense to AGPL-3.0-or-later and add SPDX headers
Replaces LICENSE (GPLv3 -> AGPLv3) and prepends
`SPDX-License-Identifier: AGPL-3.0-or-later` to every source file
across decnet/, decnet_web/, tests/, scripts/, and tools/.

Rationale: closes the GPLv3 ASP loophole so any party operating a
modified DECNET as a network service must offer their modified
source. Personal copyright (Samuel Paschuan) + inbound=outbound
contributions make a future unilateral relicense infeasible.

- LICENSE: full AGPL-3.0 text (gnu.org/licenses/agpl-3.0.txt)
- COPYRIGHT: project copyright notice
- tools/add_spdx_headers.py: idempotent header injector
  (shebang- and PEP 263-aware)

Touches 1565 source files (.py, .ts, .tsx, .js, .jsx, .css, .sh).
No behavior change; comments only.
2026-05-22 21:04:16 -04:00

89 lines
2.9 KiB
Python

# SPDX-License-Identifier: AGPL-3.0-or-later
"""DeckyLifecycle table + DTOs.
Tracks one row per (decky, operation) attempt — `deploy` or `mutate` —
so the API can return 202 Accepted immediately and the wizard can poll
state instead of holding an open HTTP request open for minutes.
State machine: ``pending`` (row created, runner not yet started) →
``running`` (runner picked it up) → terminal ``succeeded`` | ``failed``
(+ ``error`` text). Rows are immutable after terminal status; a retry
writes a new row.
Sibling of DeckyShard rather than a rework — DeckyShard tracks runtime
container state observed via heartbeat, this tracks operation lifecycle.
Per ``feedback_uuid_over_natural_keys``: new use case, new table, UUID PK.
"""
from __future__ import annotations
import uuid
from datetime import datetime, timezone
from typing import Literal, Optional
from pydantic import BaseModel, Field as PydanticField
from sqlalchemy import Column, Text
from sqlmodel import Field, SQLModel
LifecycleOperation = Literal["deploy", "mutate"]
LifecycleStatus = Literal["pending", "running", "succeeded", "failed"]
def _now_utc() -> datetime:
return datetime.now(timezone.utc)
class DeckyLifecycle(SQLModel, table=True):
"""One row per (decky, operation) attempt."""
__tablename__ = "decky_lifecycle"
id: str = Field(
primary_key=True,
default_factory=lambda: str(uuid.uuid4()),
)
decky_name: str = Field(index=True)
# None for unihost / master-resident deckies.
host_uuid: Optional[str] = Field(default=None, index=True)
operation: str = Field(index=True) # LifecycleOperation
status: str = Field(default="pending", index=True) # LifecycleStatus
error: Optional[str] = Field(
default=None, sa_column=Column("error", Text, nullable=True),
)
started_at: datetime = Field(default_factory=_now_utc)
updated_at: datetime = Field(default_factory=_now_utc)
completed_at: Optional[datetime] = Field(default=None)
# --- HTTP DTOs ---
class DeckyLifecycleView(BaseModel):
"""One lifecycle row, serialised for the wizard polling loop."""
id: str
decky_name: str
host_uuid: Optional[str] = None
operation: str
status: str
error: Optional[str] = None
started_at: datetime
updated_at: datetime
completed_at: Optional[datetime] = None
class DeckyLifecycleListResponse(BaseModel):
rows: list[DeckyLifecycleView] = PydanticField(default_factory=list)
class LifecycleAcceptedResponse(BaseModel):
"""Returned by 202 deploy/mutate endpoints — lets the client subscribe
to the matching DeckyLifecycle rows via the polling endpoint."""
lifecycle_ids: list[str]
class LifecycleDelta(BaseModel):
"""One per-decky completion record in a worker → master heartbeat."""
decky_name: str
operation: str
status: str # one of LifecycleStatus, typically "succeeded" | "failed"
error: Optional[str] = None
completed_at: Optional[datetime] = None