Files
meida_backend/packages/domain/task/state.py
T
xsl 12583e9739 feat(shared): cross-cutting plumbing and pure domain layer
Typed pydantic-settings config, structlog JSON logging with request/trace id
propagation and redaction of phone numbers, tokens and face keys, the unified
response envelope and error-code table from 架构文档 §9.2, async SQLAlchemy
session management, ORM models for the eight foundation tables, and the
OpenTelemetry/Prometheus wiring.

packages/domain stays pure logic -- snowflake ids, the quota three-phase
protocol and the tk_task state machine -- so it can be tested without a
database and is held to mypy strict.
2026-08-14 00:26:39 +08:00

77 lines
2.4 KiB
Python
Executable File

"""tk_task state machine — pure logic (架构文档 §6.2 task 域, FR-TASK-01).
统一任务中心 is the only long-running-task view in the system; every long task
(report / tryon / hairstyle / matting / render) moves through these states.
"""
from __future__ import annotations
from enum import StrEnum
from packages.domain.errors import InvalidTaskTransitionError
class TaskStatus(StrEnum):
PENDING = "pending" # created, quota reserved, not yet queued
QUEUED = "queued" # handed to Celery
RUNNING = "running"
SUCCEEDED = "succeeded"
FAILED = "failed"
CANCELED = "canceled"
class TaskType(StrEnum):
REPORT = "report"
TRYON = "tryon"
HAIRSTYLE = "hairstyle"
MATTING = "matting"
RENDER = "render"
PING = "ping" # scaffold-only: proves the async path end to end
TERMINAL_STATUSES: frozenset[TaskStatus] = frozenset(
{TaskStatus.SUCCEEDED, TaskStatus.FAILED, TaskStatus.CANCELED}
)
#: Refunds are owed for every terminal state other than success (FR-TRY-05).
REFUNDABLE_STATUSES: frozenset[TaskStatus] = frozenset({TaskStatus.FAILED, TaskStatus.CANCELED})
ALLOWED_TRANSITIONS: dict[TaskStatus, frozenset[TaskStatus]] = {
TaskStatus.PENDING: frozenset({TaskStatus.QUEUED, TaskStatus.FAILED, TaskStatus.CANCELED}),
TaskStatus.QUEUED: frozenset({TaskStatus.RUNNING, TaskStatus.FAILED, TaskStatus.CANCELED}),
TaskStatus.RUNNING: frozenset(
{TaskStatus.SUCCEEDED, TaskStatus.FAILED, TaskStatus.CANCELED, TaskStatus.QUEUED}
),
TaskStatus.SUCCEEDED: frozenset(),
TaskStatus.FAILED: frozenset({TaskStatus.QUEUED}), # retry re-queues
TaskStatus.CANCELED: frozenset(),
}
def can_transition(current: TaskStatus, target: TaskStatus) -> bool:
return target in ALLOWED_TRANSITIONS[current]
def assert_transition(current: TaskStatus, target: TaskStatus) -> None:
if not can_transition(current, target):
raise InvalidTaskTransitionError(current=current.value, target=target.value)
def is_terminal(status: TaskStatus) -> bool:
return status in TERMINAL_STATUSES
def needs_refund(status: TaskStatus) -> bool:
return status in REFUNDABLE_STATUSES
def progress_for(status: TaskStatus) -> int:
return {
TaskStatus.PENDING: 0,
TaskStatus.QUEUED: 5,
TaskStatus.RUNNING: 50,
TaskStatus.SUCCEEDED: 100,
TaskStatus.FAILED: 100,
TaskStatus.CANCELED: 100,
}[status]