mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-31 13:07:56 +08:00
198 lines
7.6 KiB
Python
198 lines
7.6 KiB
Python
"""工作流查询与执行状态事务适配器。"""
|
|
|
|
from collections.abc import Callable, Iterable
|
|
from contextlib import AbstractAsyncContextManager
|
|
from copy import deepcopy
|
|
from typing import Any, Optional, TypeVar, cast
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.application.workflow import WorkflowExecutionCommand, WorkflowSnapshot
|
|
from app.db.oper.workflow import WorkflowOper
|
|
from app.db.uow import SqlAlchemyUnitOfWork
|
|
from app.schemas.common import JsonData
|
|
|
|
_Result = TypeVar("_Result")
|
|
|
|
|
|
def _copy_json_mapping(value: object, field_name: str) -> dict[str, JsonData]:
|
|
"""复制 ORM JSON 对象,拒绝把损坏结构带出 Session。"""
|
|
if value is None:
|
|
return {}
|
|
if not isinstance(value, dict):
|
|
raise ValueError(f"工作流 {field_name} 必须是 JSON 对象")
|
|
return cast(dict[str, JsonData], deepcopy(value))
|
|
|
|
|
|
def _copy_json_sequence(
|
|
value: object,
|
|
field_name: str,
|
|
) -> tuple[dict[str, JsonData], ...]:
|
|
"""复制 ORM JSON 对象序列,确保快照不共享可变容器。"""
|
|
if value is None:
|
|
return ()
|
|
if not isinstance(value, list) or any(not isinstance(item, dict) for item in value):
|
|
raise ValueError(f"工作流 {field_name} 必须是 JSON 对象数组")
|
|
return tuple(cast(dict[str, JsonData], deepcopy(item)) for item in value)
|
|
|
|
|
|
def _project_workflow(record: object) -> WorkflowSnapshot:
|
|
"""在持有数据库会话时把 ORM 记录投影为稳定快照。"""
|
|
workflow_id = getattr(record, "id", None)
|
|
name = getattr(record, "name", None)
|
|
state = getattr(record, "state", None)
|
|
if not isinstance(workflow_id, int) or not isinstance(name, str) or not isinstance(state, str):
|
|
raise ValueError("工作流记录缺少稳定身份或状态")
|
|
return WorkflowSnapshot(
|
|
id=workflow_id,
|
|
name=name,
|
|
description=getattr(record, "description", None),
|
|
timer=getattr(record, "timer", None),
|
|
trigger_type=getattr(record, "trigger_type", None),
|
|
event_type=getattr(record, "event_type", None),
|
|
event_conditions=_copy_json_mapping(
|
|
getattr(record, "event_conditions", None),
|
|
"event_conditions",
|
|
),
|
|
state=state,
|
|
current_action=getattr(record, "current_action", None),
|
|
result=getattr(record, "result", None),
|
|
run_count=getattr(record, "run_count", None),
|
|
actions=_copy_json_sequence(getattr(record, "actions", None), "actions"),
|
|
flows=_copy_json_sequence(getattr(record, "flows", None), "flows"),
|
|
context=_copy_json_mapping(getattr(record, "context", None), "context"),
|
|
execution_config=_copy_json_mapping(
|
|
getattr(record, "execution_config", None),
|
|
"execution_config",
|
|
),
|
|
execution_state=_copy_json_mapping(
|
|
getattr(record, "execution_state", None),
|
|
"execution_state",
|
|
),
|
|
add_time=getattr(record, "add_time", None),
|
|
last_time=getattr(record, "last_time", None),
|
|
)
|
|
|
|
|
|
class TransactionalWorkflowQueryRepository:
|
|
"""在自有短 Session 内查询并投影工作流快照。"""
|
|
|
|
def __init__(
|
|
self,
|
|
sync_session: Callable[[], Session],
|
|
async_session: Callable[[], AbstractAsyncContextManager[AsyncSession]],
|
|
) -> None:
|
|
"""保存同步 Session 工厂与异步 Session 作用域。"""
|
|
self._sync_session = sync_session
|
|
self._async_session = async_session
|
|
|
|
def get(self, workflow_id: int) -> Optional[WorkflowSnapshot]:
|
|
"""在同步短 Session 内读取并投影单条工作流。"""
|
|
session = self._sync_session()
|
|
try:
|
|
record = WorkflowOper(session).get(workflow_id)
|
|
return _project_workflow(record) if record else None
|
|
finally:
|
|
session.close()
|
|
|
|
def list_enabled(self) -> list[WorkflowSnapshot]:
|
|
"""在同步短 Session 内投影全部启用工作流。"""
|
|
return self._list_sync(lambda repository: repository.list_enabled())
|
|
|
|
def list_timer_enabled(self) -> list[WorkflowSnapshot]:
|
|
"""在同步短 Session 内投影启用的定时工作流。"""
|
|
return self._list_sync(
|
|
lambda repository: repository.get_timer_triggered_workflows()
|
|
)
|
|
|
|
def list_event_enabled(self) -> list[WorkflowSnapshot]:
|
|
"""在同步短 Session 内投影启用的事件工作流。"""
|
|
return self._list_sync(
|
|
lambda repository: repository.get_event_triggered_workflows()
|
|
)
|
|
|
|
async def async_list(self) -> list[WorkflowSnapshot]:
|
|
"""在异步短 Session 内投影全部工作流。"""
|
|
async with self._async_session() as session:
|
|
records = await WorkflowOper(session).async_list()
|
|
return [_project_workflow(record) for record in records]
|
|
|
|
async def async_get(self, workflow_id: int) -> Optional[WorkflowSnapshot]:
|
|
"""在异步短 Session 内读取并投影单条工作流。"""
|
|
async with self._async_session() as session:
|
|
record = await WorkflowOper(session).async_get(workflow_id)
|
|
return _project_workflow(record) if record else None
|
|
|
|
def _list_sync(
|
|
self,
|
|
operation: Callable[[WorkflowOper], Iterable[object]],
|
|
) -> list[WorkflowSnapshot]:
|
|
"""在同步短 Session 内执行列表查询并完成投影。"""
|
|
session = self._sync_session()
|
|
try:
|
|
return [
|
|
_project_workflow(record)
|
|
for record in operation(WorkflowOper(session))
|
|
]
|
|
finally:
|
|
session.close()
|
|
|
|
|
|
class TransactionalWorkflowExecutionService:
|
|
"""为每次工作流执行状态写入创建独立短会话和 UnitOfWork。"""
|
|
|
|
def __init__(self, session_factory: Callable[[], Session]) -> None:
|
|
"""保存由启动组合根提供的同步 Session 工厂。"""
|
|
self._session_factory = session_factory
|
|
|
|
def start(self, workflow_id: int) -> bool:
|
|
"""以独立事务提交运行中状态。"""
|
|
return self._run(lambda command: command.start(workflow_id))
|
|
|
|
def success(self, workflow_id: int, result: str | None = None) -> bool:
|
|
"""以独立事务提交成功状态。"""
|
|
return self._run(lambda command: command.success(workflow_id, result))
|
|
|
|
def fail(self, workflow_id: int, result: str) -> bool:
|
|
"""以独立事务提交失败状态。"""
|
|
return self._run(lambda command: command.fail(workflow_id, result))
|
|
|
|
def step(
|
|
self,
|
|
workflow_id: int,
|
|
action_id: str,
|
|
context: dict[str, Any],
|
|
execution_state: dict[str, Any] | None = None,
|
|
) -> bool:
|
|
"""以独立事务提交动作进度。"""
|
|
return self._run(
|
|
lambda command: command.step(
|
|
workflow_id,
|
|
action_id,
|
|
context,
|
|
execution_state,
|
|
)
|
|
)
|
|
|
|
def reset(self, workflow_id: int, reset_count: bool = False) -> bool:
|
|
"""以独立事务提交执行状态重置。"""
|
|
return self._run(
|
|
lambda command: command.reset(workflow_id, reset_count)
|
|
)
|
|
|
|
def _run(
|
|
self,
|
|
operation: Callable[[WorkflowExecutionCommand], _Result],
|
|
) -> _Result:
|
|
"""创建短会话并把提交/回滚交给 Application command。"""
|
|
session = self._session_factory()
|
|
try:
|
|
command = WorkflowExecutionCommand(
|
|
repository=WorkflowOper(db=session),
|
|
unit_of_work=SqlAlchemyUnitOfWork(session),
|
|
)
|
|
return operation(command)
|
|
finally:
|
|
session.close()
|