合并
This commit is contained in:
20
etl_billiards/gui/models/__init__.py
Normal file
20
etl_billiards/gui/models/__init__.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""数据模型模块"""
|
||||
|
||||
from .task_model import TaskItem, TaskStatus, TaskHistory, TaskConfig, QueuedTask
|
||||
from .schedule_model import (
|
||||
ScheduledTask, ScheduleConfig, ScheduleType, IntervalUnit, ScheduleStore
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"TaskItem",
|
||||
"TaskStatus",
|
||||
"TaskHistory",
|
||||
"TaskConfig",
|
||||
"QueuedTask",
|
||||
"ScheduledTask",
|
||||
"ScheduleConfig",
|
||||
"ScheduleType",
|
||||
"IntervalUnit",
|
||||
"ScheduleStore",
|
||||
]
|
||||
391
etl_billiards/gui/models/schedule_model.py
Normal file
391
etl_billiards/gui/models/schedule_model.py
Normal file
@@ -0,0 +1,391 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""调度任务数据模型"""
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass, field, asdict
|
||||
from datetime import datetime, timedelta
|
||||
from enum import Enum
|
||||
from typing import Optional, List, Dict, Any
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class ScheduleType(Enum):
|
||||
"""调度类型"""
|
||||
ONCE = "once" # 一次性
|
||||
INTERVAL = "interval" # 固定间隔
|
||||
DAILY = "daily" # 每天
|
||||
WEEKLY = "weekly" # 每周
|
||||
CRON = "cron" # Cron 表达式
|
||||
|
||||
|
||||
class IntervalUnit(Enum):
|
||||
"""间隔单位"""
|
||||
MINUTES = "minutes"
|
||||
HOURS = "hours"
|
||||
DAYS = "days"
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScheduleConfig:
|
||||
"""调度配置"""
|
||||
schedule_type: ScheduleType = ScheduleType.ONCE
|
||||
|
||||
# 间隔调度
|
||||
interval_value: int = 1
|
||||
interval_unit: IntervalUnit = IntervalUnit.HOURS
|
||||
|
||||
# 每日调度
|
||||
daily_time: str = "04:00" # HH:MM
|
||||
|
||||
# 每周调度
|
||||
weekly_days: List[int] = field(default_factory=lambda: [1]) # 1-7, 1=周一
|
||||
weekly_time: str = "04:00"
|
||||
|
||||
# Cron 表达式
|
||||
cron_expression: str = "0 4 * * *"
|
||||
|
||||
# 通用设置
|
||||
enabled: bool = True
|
||||
start_date: Optional[str] = None # YYYY-MM-DD
|
||||
end_date: Optional[str] = None # YYYY-MM-DD
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""转换为字典"""
|
||||
return {
|
||||
"schedule_type": self.schedule_type.value,
|
||||
"interval_value": self.interval_value,
|
||||
"interval_unit": self.interval_unit.value,
|
||||
"daily_time": self.daily_time,
|
||||
"weekly_days": self.weekly_days,
|
||||
"weekly_time": self.weekly_time,
|
||||
"cron_expression": self.cron_expression,
|
||||
"enabled": self.enabled,
|
||||
"start_date": self.start_date,
|
||||
"end_date": self.end_date,
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict) -> "ScheduleConfig":
|
||||
"""从字典创建"""
|
||||
return cls(
|
||||
schedule_type=ScheduleType(data.get("schedule_type", "once")),
|
||||
interval_value=data.get("interval_value", 1),
|
||||
interval_unit=IntervalUnit(data.get("interval_unit", "hours")),
|
||||
daily_time=data.get("daily_time", "04:00"),
|
||||
weekly_days=data.get("weekly_days", [1]),
|
||||
weekly_time=data.get("weekly_time", "04:00"),
|
||||
cron_expression=data.get("cron_expression", "0 4 * * *"),
|
||||
enabled=data.get("enabled", True),
|
||||
start_date=data.get("start_date"),
|
||||
end_date=data.get("end_date"),
|
||||
)
|
||||
|
||||
def get_description(self) -> str:
|
||||
"""获取调度描述"""
|
||||
if self.schedule_type == ScheduleType.ONCE:
|
||||
return "一次性执行"
|
||||
elif self.schedule_type == ScheduleType.INTERVAL:
|
||||
unit_names = {"minutes": "分钟", "hours": "小时", "days": "天"}
|
||||
return f"每 {self.interval_value} {unit_names[self.interval_unit.value]}"
|
||||
elif self.schedule_type == ScheduleType.DAILY:
|
||||
return f"每天 {self.daily_time}"
|
||||
elif self.schedule_type == ScheduleType.WEEKLY:
|
||||
day_names = {1: "一", 2: "二", 3: "三", 4: "四", 5: "五", 6: "六", 7: "日"}
|
||||
days = "、".join(f"周{day_names[d]}" for d in sorted(self.weekly_days))
|
||||
return f"每周 {days} {self.weekly_time}"
|
||||
elif self.schedule_type == ScheduleType.CRON:
|
||||
return f"Cron: {self.cron_expression}"
|
||||
return "未知"
|
||||
|
||||
# 首次执行延迟秒数
|
||||
FIRST_RUN_DELAY_SECONDS = 60
|
||||
|
||||
def get_next_run_time(self, last_run: Optional[datetime] = None) -> Optional[datetime]:
|
||||
"""计算下次运行时间
|
||||
|
||||
注意:首次执行(last_run 为 None)时会延迟 60 秒,避免创建后立即执行
|
||||
"""
|
||||
now = datetime.now()
|
||||
|
||||
# 检查日期范围
|
||||
if self.start_date:
|
||||
start = datetime.strptime(self.start_date, "%Y-%m-%d")
|
||||
if now < start:
|
||||
now = start
|
||||
|
||||
if self.end_date:
|
||||
end = datetime.strptime(self.end_date, "%Y-%m-%d") + timedelta(days=1)
|
||||
if now >= end:
|
||||
return None
|
||||
|
||||
# 首次执行延迟 60 秒
|
||||
first_run_time = now + timedelta(seconds=self.FIRST_RUN_DELAY_SECONDS)
|
||||
|
||||
if self.schedule_type == ScheduleType.ONCE:
|
||||
return None if last_run else first_run_time
|
||||
|
||||
elif self.schedule_type == ScheduleType.INTERVAL:
|
||||
if not last_run:
|
||||
return first_run_time
|
||||
if self.interval_unit == IntervalUnit.MINUTES:
|
||||
delta = timedelta(minutes=self.interval_value)
|
||||
elif self.interval_unit == IntervalUnit.HOURS:
|
||||
delta = timedelta(hours=self.interval_value)
|
||||
else:
|
||||
delta = timedelta(days=self.interval_value)
|
||||
return last_run + delta
|
||||
|
||||
elif self.schedule_type == ScheduleType.DAILY:
|
||||
hour, minute = map(int, self.daily_time.split(":"))
|
||||
next_run = now.replace(hour=hour, minute=minute, second=0, microsecond=0)
|
||||
if next_run <= now:
|
||||
next_run += timedelta(days=1)
|
||||
return next_run
|
||||
|
||||
elif self.schedule_type == ScheduleType.WEEKLY:
|
||||
hour, minute = map(int, self.weekly_time.split(":"))
|
||||
# 找到下一个匹配的日期
|
||||
for i in range(8):
|
||||
check_date = now + timedelta(days=i)
|
||||
weekday = check_date.isoweekday() # 1-7
|
||||
if weekday in self.weekly_days:
|
||||
next_run = check_date.replace(hour=hour, minute=minute, second=0, microsecond=0)
|
||||
if next_run > now:
|
||||
return next_run
|
||||
return None
|
||||
|
||||
elif self.schedule_type == ScheduleType.CRON:
|
||||
# 简化版 Cron 解析(只支持基本格式)
|
||||
try:
|
||||
return self._parse_simple_cron(now)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
return None
|
||||
|
||||
def _parse_simple_cron(self, now: datetime) -> Optional[datetime]:
|
||||
"""简化版 Cron 解析"""
|
||||
parts = self.cron_expression.split()
|
||||
if len(parts) != 5:
|
||||
return None
|
||||
|
||||
minute, hour, day, month, weekday = parts
|
||||
|
||||
# 只处理简单情况
|
||||
if minute.isdigit() and hour.isdigit():
|
||||
next_run = now.replace(
|
||||
hour=int(hour),
|
||||
minute=int(minute),
|
||||
second=0,
|
||||
microsecond=0
|
||||
)
|
||||
if next_run <= now:
|
||||
next_run += timedelta(days=1)
|
||||
return next_run
|
||||
|
||||
return None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScheduleExecutionRecord:
|
||||
"""调度执行记录"""
|
||||
task_id: str # 关联的 QueuedTask ID
|
||||
executed_at: datetime # 执行时间
|
||||
status: str = "" # 状态:success, failed, pending
|
||||
exit_code: Optional[int] = None # 退出码
|
||||
duration_seconds: float = 0.0 # 耗时(秒)
|
||||
summary: str = "" # 执行摘要
|
||||
output: str = "" # 完整执行日志
|
||||
error: str = "" # 错误信息
|
||||
|
||||
# 日志最大长度限制(字符数)
|
||||
MAX_OUTPUT_LENGTH: int = 100000 # 100KB
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
return {
|
||||
"task_id": self.task_id,
|
||||
"executed_at": self.executed_at.isoformat(),
|
||||
"status": self.status,
|
||||
"exit_code": self.exit_code,
|
||||
"duration_seconds": self.duration_seconds,
|
||||
"summary": self.summary,
|
||||
"output": self.output[:self.MAX_OUTPUT_LENGTH] if self.output else "",
|
||||
"error": self.error[:5000] if self.error else "",
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict) -> "ScheduleExecutionRecord":
|
||||
return cls(
|
||||
task_id=data.get("task_id", ""),
|
||||
executed_at=datetime.fromisoformat(data["executed_at"]) if data.get("executed_at") else datetime.now(),
|
||||
status=data.get("status", ""),
|
||||
exit_code=data.get("exit_code"),
|
||||
duration_seconds=data.get("duration_seconds", 0.0),
|
||||
summary=data.get("summary", ""),
|
||||
output=data.get("output", ""),
|
||||
error=data.get("error", ""),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScheduledTask:
|
||||
"""调度任务"""
|
||||
id: str
|
||||
name: str
|
||||
task_codes: List[str]
|
||||
schedule: ScheduleConfig
|
||||
task_config: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
# 运行状态
|
||||
enabled: bool = True
|
||||
last_run: Optional[datetime] = None
|
||||
next_run: Optional[datetime] = None
|
||||
run_count: int = 0
|
||||
last_status: str = ""
|
||||
|
||||
# 执行历史(最近 N 次执行记录)
|
||||
execution_history: List[ScheduleExecutionRecord] = field(default_factory=list)
|
||||
MAX_HISTORY_SIZE: int = field(default=50, repr=False) # 保留最近50次执行记录
|
||||
|
||||
created_at: datetime = field(default_factory=datetime.now)
|
||||
updated_at: datetime = field(default_factory=datetime.now)
|
||||
|
||||
def add_execution_record(self, record: ScheduleExecutionRecord):
|
||||
"""添加执行记录"""
|
||||
self.execution_history.insert(0, record)
|
||||
# 限制历史记录数量
|
||||
if len(self.execution_history) > self.MAX_HISTORY_SIZE:
|
||||
self.execution_history = self.execution_history[:self.MAX_HISTORY_SIZE]
|
||||
|
||||
def update_execution_record(self, task_id: str, status: str, exit_code: int, duration: float,
|
||||
summary: str, output: str = "", error: str = ""):
|
||||
"""更新执行记录状态"""
|
||||
for record in self.execution_history:
|
||||
if record.task_id == task_id:
|
||||
record.status = status
|
||||
record.exit_code = exit_code
|
||||
record.duration_seconds = duration
|
||||
record.summary = summary
|
||||
record.output = output
|
||||
record.error = error
|
||||
break
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""转换为字典"""
|
||||
return {
|
||||
"id": self.id,
|
||||
"name": self.name,
|
||||
"task_codes": self.task_codes,
|
||||
"schedule": self.schedule.to_dict(),
|
||||
"task_config": self.task_config,
|
||||
"enabled": self.enabled,
|
||||
"last_run": self.last_run.isoformat() if self.last_run else None,
|
||||
"next_run": self.next_run.isoformat() if self.next_run else None,
|
||||
"run_count": self.run_count,
|
||||
"last_status": self.last_status,
|
||||
"execution_history": [r.to_dict() for r in self.execution_history],
|
||||
"created_at": self.created_at.isoformat(),
|
||||
"updated_at": self.updated_at.isoformat(),
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict) -> "ScheduledTask":
|
||||
"""从字典创建"""
|
||||
history_data = data.get("execution_history", [])
|
||||
execution_history = [ScheduleExecutionRecord.from_dict(r) for r in history_data]
|
||||
|
||||
return cls(
|
||||
id=data["id"],
|
||||
name=data["name"],
|
||||
task_codes=data["task_codes"],
|
||||
schedule=ScheduleConfig.from_dict(data.get("schedule", {})),
|
||||
task_config=data.get("task_config", {}),
|
||||
enabled=data.get("enabled", True),
|
||||
last_run=datetime.fromisoformat(data["last_run"]) if data.get("last_run") else None,
|
||||
next_run=datetime.fromisoformat(data["next_run"]) if data.get("next_run") else None,
|
||||
run_count=data.get("run_count", 0),
|
||||
last_status=data.get("last_status", ""),
|
||||
execution_history=execution_history,
|
||||
created_at=datetime.fromisoformat(data["created_at"]) if data.get("created_at") else datetime.now(),
|
||||
updated_at=datetime.fromisoformat(data["updated_at"]) if data.get("updated_at") else datetime.now(),
|
||||
)
|
||||
|
||||
def update_next_run(self):
|
||||
"""更新下次运行时间"""
|
||||
if self.enabled and self.schedule.enabled:
|
||||
self.next_run = self.schedule.get_next_run_time(self.last_run)
|
||||
else:
|
||||
self.next_run = None
|
||||
self.updated_at = datetime.now()
|
||||
|
||||
|
||||
class ScheduleStore:
|
||||
"""调度任务存储"""
|
||||
|
||||
def __init__(self, storage_path: Optional[Path] = None):
|
||||
if storage_path is None:
|
||||
storage_path = Path(__file__).resolve().parents[2] / "scheduled_tasks.json"
|
||||
self.storage_path = storage_path
|
||||
self.tasks: Dict[str, ScheduledTask] = {}
|
||||
self.load()
|
||||
|
||||
def load(self):
|
||||
"""加载任务"""
|
||||
if self.storage_path.exists():
|
||||
try:
|
||||
data = json.loads(self.storage_path.read_text(encoding="utf-8"))
|
||||
self.tasks = {
|
||||
task_id: ScheduledTask.from_dict(task_data)
|
||||
for task_id, task_data in data.get("tasks", {}).items()
|
||||
}
|
||||
except Exception:
|
||||
self.tasks = {}
|
||||
|
||||
def save(self):
|
||||
"""保存任务"""
|
||||
data = {
|
||||
"tasks": {
|
||||
task_id: task.to_dict()
|
||||
for task_id, task in self.tasks.items()
|
||||
}
|
||||
}
|
||||
self.storage_path.write_text(
|
||||
json.dumps(data, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8"
|
||||
)
|
||||
|
||||
def add_task(self, task: ScheduledTask):
|
||||
"""添加任务"""
|
||||
task.update_next_run()
|
||||
self.tasks[task.id] = task
|
||||
self.save()
|
||||
|
||||
def remove_task(self, task_id: str):
|
||||
"""移除任务"""
|
||||
if task_id in self.tasks:
|
||||
del self.tasks[task_id]
|
||||
self.save()
|
||||
|
||||
def update_task(self, task: ScheduledTask):
|
||||
"""更新任务"""
|
||||
task.update_next_run()
|
||||
task.updated_at = datetime.now()
|
||||
self.tasks[task.id] = task
|
||||
self.save()
|
||||
|
||||
def get_task(self, task_id: str) -> Optional[ScheduledTask]:
|
||||
"""获取任务"""
|
||||
return self.tasks.get(task_id)
|
||||
|
||||
def get_all_tasks(self) -> List[ScheduledTask]:
|
||||
"""获取所有任务"""
|
||||
return list(self.tasks.values())
|
||||
|
||||
def get_due_tasks(self) -> List[ScheduledTask]:
|
||||
"""获取到期需要执行的任务"""
|
||||
now = datetime.now()
|
||||
due_tasks = []
|
||||
for task in self.tasks.values():
|
||||
if task.enabled and task.next_run and task.next_run <= now:
|
||||
due_tasks.append(task)
|
||||
return due_tasks
|
||||
179
etl_billiards/gui/models/task_model.py
Normal file
179
etl_billiards/gui/models/task_model.py
Normal file
@@ -0,0 +1,179 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""任务数据模型"""
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from typing import Optional, List, Dict, Any
|
||||
|
||||
|
||||
class TaskStatus(Enum):
|
||||
"""任务状态枚举"""
|
||||
PENDING = "pending" # 待执行
|
||||
RUNNING = "running" # 执行中
|
||||
SUCCESS = "success" # 成功
|
||||
FAILED = "failed" # 失败
|
||||
CANCELLED = "cancelled" # 已取消
|
||||
|
||||
|
||||
class TaskCategory(Enum):
|
||||
"""任务分类"""
|
||||
ODS = "ODS" # ODS 数据抓取任务
|
||||
DWD = "DWD" # DWD 装载任务
|
||||
DWS = "DWS" # DWS 汇总任务
|
||||
SCHEMA = "Schema" # Schema 初始化任务
|
||||
QUALITY = "Quality" # 质量检查任务
|
||||
OTHER = "Other" # 其他任务
|
||||
|
||||
|
||||
# 任务分类映射
|
||||
TASK_CATEGORIES: Dict[str, TaskCategory] = {
|
||||
# ODS 任务
|
||||
"ODS_PAYMENT": TaskCategory.ODS,
|
||||
"ODS_MEMBER": TaskCategory.ODS,
|
||||
"ODS_MEMBER_CARD": TaskCategory.ODS,
|
||||
"ODS_MEMBER_BALANCE": TaskCategory.ODS,
|
||||
"ODS_SETTLEMENT_RECORDS": TaskCategory.ODS,
|
||||
"ODS_TABLE_USE": TaskCategory.ODS,
|
||||
"ODS_ASSISTANT_ACCOUNT": TaskCategory.ODS,
|
||||
"ODS_ASSISTANT_LEDGER": TaskCategory.ODS,
|
||||
"ODS_ASSISTANT_ABOLISH": TaskCategory.ODS,
|
||||
"ODS_REFUND": TaskCategory.ODS,
|
||||
"ODS_PLATFORM_COUPON": TaskCategory.ODS,
|
||||
"ODS_RECHARGE_SETTLE": TaskCategory.ODS,
|
||||
"ODS_GROUP_PACKAGE": TaskCategory.ODS,
|
||||
"ODS_GROUP_BUY_REDEMPTION": TaskCategory.ODS,
|
||||
"ODS_INVENTORY_STOCK": TaskCategory.ODS,
|
||||
"ODS_INVENTORY_CHANGE": TaskCategory.ODS,
|
||||
"ODS_TABLES": TaskCategory.ODS,
|
||||
"ODS_GOODS_CATEGORY": TaskCategory.ODS,
|
||||
"ODS_STORE_GOODS": TaskCategory.ODS,
|
||||
"ODS_STORE_GOODS_SALES": TaskCategory.ODS,
|
||||
"ODS_TABLE_FEE_DISCOUNT": TaskCategory.ODS,
|
||||
"ODS_TENANT_GOODS": TaskCategory.ODS,
|
||||
"ODS_SETTLEMENT_TICKET": TaskCategory.ODS,
|
||||
# DWD 任务
|
||||
"DWD_LOAD_FROM_ODS": TaskCategory.DWD,
|
||||
"DWD_QUALITY_CHECK": TaskCategory.QUALITY,
|
||||
"PAYMENTS_DWD": TaskCategory.DWD,
|
||||
"MEMBERS_DWD": TaskCategory.DWD,
|
||||
"TICKET_DWD": TaskCategory.DWD,
|
||||
# DWS 任务
|
||||
"INIT_DWS_SCHEMA": TaskCategory.SCHEMA,
|
||||
"DWS_BUILD_ORDER_SUMMARY": TaskCategory.DWS,
|
||||
# Schema 任务
|
||||
"INIT_ODS_SCHEMA": TaskCategory.SCHEMA,
|
||||
"INIT_DWD_SCHEMA": TaskCategory.SCHEMA,
|
||||
# 其他任务
|
||||
"MANUAL_INGEST": TaskCategory.OTHER,
|
||||
"CHECK_CUTOFF": TaskCategory.OTHER,
|
||||
"DATA_INTEGRITY_CHECK": TaskCategory.QUALITY,
|
||||
"ODS_JSON_ARCHIVE": TaskCategory.OTHER,
|
||||
# 旧版任务(兼容)
|
||||
"PRODUCTS": TaskCategory.ODS,
|
||||
"TABLES": TaskCategory.ODS,
|
||||
"MEMBERS": TaskCategory.ODS,
|
||||
"ASSISTANTS": TaskCategory.ODS,
|
||||
"PACKAGES_DEF": TaskCategory.ODS,
|
||||
"ORDERS": TaskCategory.ODS,
|
||||
"PAYMENTS": TaskCategory.ODS,
|
||||
"REFUNDS": TaskCategory.ODS,
|
||||
"COUPON_USAGE": TaskCategory.ODS,
|
||||
"INVENTORY_CHANGE": TaskCategory.ODS,
|
||||
"TOPUPS": TaskCategory.ODS,
|
||||
"TABLE_DISCOUNT": TaskCategory.ODS,
|
||||
"ASSISTANT_ABOLISH": TaskCategory.ODS,
|
||||
"LEDGER": TaskCategory.ODS,
|
||||
}
|
||||
|
||||
|
||||
def get_task_category(task_code: str) -> TaskCategory:
|
||||
"""获取任务分类"""
|
||||
return TASK_CATEGORIES.get(task_code.upper(), TaskCategory.OTHER)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TaskItem:
|
||||
"""任务项"""
|
||||
task_code: str
|
||||
name: str = ""
|
||||
description: str = ""
|
||||
category: TaskCategory = TaskCategory.OTHER
|
||||
enabled: bool = True
|
||||
|
||||
def __post_init__(self):
|
||||
if not self.name:
|
||||
self.name = self.task_code
|
||||
if not self.category or self.category == TaskCategory.OTHER:
|
||||
self.category = get_task_category(self.task_code)
|
||||
|
||||
|
||||
@dataclass
|
||||
class TaskConfig:
|
||||
"""任务执行配置"""
|
||||
tasks: List[str] = field(default_factory=list)
|
||||
pipeline_flow: str = "FULL" # FULL, FETCH_ONLY, INGEST_ONLY
|
||||
dry_run: bool = False
|
||||
window_start: Optional[str] = None
|
||||
window_end: Optional[str] = None
|
||||
window_split: Optional[str] = None # none, month
|
||||
window_compensation: int = 0 # 补偿小时数
|
||||
ingest_source: Optional[str] = None
|
||||
store_id: Optional[int] = None
|
||||
pg_dsn: Optional[str] = None
|
||||
api_token: Optional[str] = None
|
||||
extra_args: Dict[str, Any] = field(default_factory=dict)
|
||||
env_vars: Dict[str, str] = field(default_factory=dict) # 额外环境变量
|
||||
|
||||
|
||||
@dataclass
|
||||
class TaskHistory:
|
||||
"""任务执行历史"""
|
||||
id: str
|
||||
task_codes: List[str]
|
||||
status: TaskStatus
|
||||
start_time: datetime
|
||||
end_time: Optional[datetime] = None
|
||||
exit_code: Optional[int] = None
|
||||
command: str = ""
|
||||
output_log: str = ""
|
||||
error_message: str = ""
|
||||
summary: Dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
@property
|
||||
def duration_seconds(self) -> Optional[float]:
|
||||
"""执行时长(秒)"""
|
||||
if self.end_time and self.start_time:
|
||||
return (self.end_time - self.start_time).total_seconds()
|
||||
return None
|
||||
|
||||
@property
|
||||
def duration_str(self) -> str:
|
||||
"""格式化的执行时长"""
|
||||
secs = self.duration_seconds
|
||||
if secs is None:
|
||||
return "-"
|
||||
if secs < 60:
|
||||
return f"{secs:.1f}秒"
|
||||
elif secs < 3600:
|
||||
mins = int(secs // 60)
|
||||
secs = secs % 60
|
||||
return f"{mins}分{secs:.0f}秒"
|
||||
else:
|
||||
hours = int(secs // 3600)
|
||||
mins = int((secs % 3600) // 60)
|
||||
return f"{hours}时{mins}分"
|
||||
|
||||
|
||||
@dataclass
|
||||
class QueuedTask:
|
||||
"""队列中的任务"""
|
||||
id: str
|
||||
config: TaskConfig
|
||||
status: TaskStatus = TaskStatus.PENDING
|
||||
created_at: datetime = field(default_factory=datetime.now)
|
||||
started_at: Optional[datetime] = None
|
||||
finished_at: Optional[datetime] = None
|
||||
output: str = ""
|
||||
error: str = ""
|
||||
exit_code: Optional[int] = None
|
||||
Reference in New Issue
Block a user