ForcePilot/backend/package/yuxi/scheduler/use_cases/mappers.py
Kris 6498af03e3 feat(scheduler): 完整实现定时任务调度限界上下文基础架构
本次提交完成了定时任务调度限界上下文的全量基础架构搭建,包括:
1.  基于六边形架构的完整分层(core/use_cases/framework/adapters/infrastructure)
2.  任务调度核心领域模型、端口契约与校验工具
3.  持久化适配器层与SQLAlchemy仓储实现
4.  调度运行时核心组件(handler注册表、执行引擎)
5.  内置维护型任务handler(日志清理、幂等记录清理)
6.  全局异常处理器与PostgreSQL表结构适配
7.  ARQ worker调度任务集成与启动装配逻辑
2026-06-22 20:07:20 +08:00

202 lines
6.9 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""领域模型(dataclass) ↔ Output DTO(Pydantic) 转换。
规范 §6.1/§7.3(对齐 ``external_systems/use_cases/mappers.py``
- persistence 返回领域模型 dataclassservice 返回 Output DTO
- scheduler 域无敏感字段mapper 仅做字段映射与 DateTime → ISO 字符串格式化
- use_cases 层不调用 ORM 的 ``to_dict`` 方法
DateTime 字段(``run_at`` / ``last_run_at`` / ``next_run_at`` / ``started_at`` /
``finished_at`` / ``created_at`` / ``updated_at`` / ``deleted_at``)在 dataclass
中以 naive UTC ``datetime`` 表达mapper 通过 ``format_utc_datetime`` 统一格式化
为 ISO 8601 字符串写入 Output DTO。
"""
from __future__ import annotations
from typing import Any
from yuxi.scheduler.core.models import (
DailyStat,
HandlerSummary,
ScheduledTask,
ScheduledTaskRunLog,
)
from yuxi.scheduler.use_cases.dto.scheduler import (
CountByStatusOutput,
DailyStatOutput,
GetHealthOutput,
HandlerSummaryOutput,
ListDailyStatsOutput,
ListHandlerSummaryOutput,
ListUpcomingOutput,
RunLogOutput,
TaskOutput,
)
from yuxi.utils.datetime_utils import format_utc_datetime
__all__ = [
"daily_stat_to_dict",
"to_count_by_status_output",
"to_daily_stat_output",
"to_get_health_output",
"to_handler_summary_output",
"to_list_daily_stats_output",
"to_run_log_output",
"to_task_output",
]
def to_task_output(task: ScheduledTask) -> TaskOutput:
"""``ScheduledTask`` dataclass → ``TaskOutput`` DTO。
DateTime 字段通过 ``format_utc_datetime`` 格式化为 ISO 8601 字符串。
"""
return TaskOutput(
task_id=task.task_id,
handler_name=task.handler_name,
owner_scope=task.owner_scope,
owner_id=task.owner_id,
schedule_kind=task.schedule_kind,
cron_expression=task.cron_expression,
run_at=format_utc_datetime(task.run_at),
tz=task.tz,
payload=task.payload or {},
enabled=task.enabled,
delete_after_run=task.delete_after_run,
block_strategy=task.block_strategy,
stagger_seconds=task.stagger_seconds,
consecutive_errors=task.consecutive_errors,
status=task.status,
last_run_at=format_utc_datetime(task.last_run_at),
next_run_at=format_utc_datetime(task.next_run_at),
last_error=task.last_error,
created_by=task.created_by,
updated_by=task.updated_by,
id=task.id,
created_at=format_utc_datetime(task.created_at),
updated_at=format_utc_datetime(task.updated_at),
is_deleted=task.is_deleted,
deleted_at=format_utc_datetime(task.deleted_at),
)
def to_run_log_output(log: ScheduledTaskRunLog) -> RunLogOutput:
"""``ScheduledTaskRunLog`` dataclass → ``RunLogOutput`` DTO。"""
return RunLogOutput(
task_id=log.task_id,
run_id=log.run_id,
triggered_by=log.triggered_by,
status=log.status,
error_message=log.error_message,
output=log.output,
started_at=format_utc_datetime(log.started_at),
finished_at=format_utc_datetime(log.finished_at),
created_by=log.created_by,
updated_by=log.updated_by,
id=log.id,
created_at=format_utc_datetime(log.created_at),
updated_at=format_utc_datetime(log.updated_at),
is_deleted=log.is_deleted,
deleted_at=format_utc_datetime(log.deleted_at),
)
def to_handler_summary_output(summary: HandlerSummary) -> HandlerSummaryOutput:
"""``HandlerSummary`` dataclass → ``HandlerSummaryOutput`` DTO。
``description`` 字段仓储层不聚合DB 无此列),由调用方按需补充;
``active_task_count`` 同理,默认 0由调用方按需补充。
"""
return HandlerSummaryOutput(
handler_name=summary.name,
task_count=summary.task_count,
last_active_at=format_utc_datetime(summary.last_active_at),
)
def to_count_by_status_output(dist: dict[str, int]) -> CountByStatusOutput:
"""状态分布字典 → ``CountByStatusOutput`` DTO。
仓储层 ``count_by_status`` 返回 ``{status: count}``,未出现的状态不在字典中,
这里统一补 0。
"""
return CountByStatusOutput(
active=dist.get("active", 0),
paused=dist.get("paused", 0),
dead_letter=dist.get("dead_letter", 0),
)
def to_get_health_output(
*,
last_tick_at: Any,
active_task_count: int,
dead_letter_count: int,
running_count: int,
healthy: bool,
) -> GetHealthOutput:
"""构造 ``GetHealthOutput`` DTO。
``healthy`` 由调用方按"最近 tick 是否在 5 分钟内"判断后传入。
"""
return GetHealthOutput(
status="healthy" if healthy else "unhealthy",
last_tick_at=format_utc_datetime(last_tick_at),
active_task_count=active_task_count,
dead_letter_count=dead_letter_count,
running_count=running_count,
)
def daily_stat_to_dict(stat: DailyStat) -> dict[str, Any]:
"""``DailyStat`` dataclass → dict供指标导出使用
指标服务(``SchedulerMetricService``)从 ``DailyStat`` 提取字段构造
Prometheus 文本格式,此处提供统一的 dict 转换避免重复字段读取。
"""
return {
"stat_date": stat.stat_date,
"handler_name": stat.handler_name,
"success_count": stat.success_count,
"failure_count": stat.failure_count,
"timeout_count": stat.timeout_count,
"dead_letter_count": stat.dead_letter_count,
}
def to_daily_stat_output(stat: DailyStat) -> DailyStatOutput:
"""``DailyStat`` dataclass → ``DailyStatOutput`` DTO。
计算派生字段 ``total_count`` / ``success_rate````stat_date`` 格式化为
ISO 日期字符串(``YYYY-MM-DD``)。
"""
total_count = stat.success_count + stat.failure_count + stat.timeout_count
success_rate = stat.success_count / total_count if total_count > 0 else 0.0
return DailyStatOutput(
stat_date=stat.stat_date.isoformat(),
handler_name=stat.handler_name,
success_count=stat.success_count,
failure_count=stat.failure_count,
timeout_count=stat.timeout_count,
dead_letter_count=stat.dead_letter_count,
total_count=total_count,
success_rate=success_rate,
)
def to_list_daily_stats_output(stats: list[DailyStat]) -> ListDailyStatsOutput:
"""``DailyStat`` 列表 → ``ListDailyStatsOutput`` DTO。"""
return ListDailyStatsOutput(items=[to_daily_stat_output(s) for s in stats])
def to_list_upcoming_output(tasks: list[ScheduledTask]) -> ListUpcomingOutput:
"""``ScheduledTask`` 列表 → ``ListUpcomingOutput`` DTO。"""
return ListUpcomingOutput(items=[to_task_output(t) for t in tasks])
def to_list_handler_summary_output(
summaries: list[HandlerSummary],
) -> ListHandlerSummaryOutput:
"""``HandlerSummary`` 列表 → ``ListHandlerSummaryOutput`` DTO。"""
return ListHandlerSummaryOutput(items=[to_handler_summary_output(s) for s in summaries])