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.
This commit is contained in:
Executable
+76
@@ -0,0 +1,76 @@
|
||||
"""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]
|
||||
Reference in New Issue
Block a user