diff --git a/AGENTS.md b/AGENTS.md index ce72eb44a..cd5eb994e 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -60,8 +60,8 @@ The legacy roots have no physical directories in the source tree. Current images |---|---|---|---| | `app/foundation/` | 无状态、无配置和无 I/O 的底层机制:反射/动态导入、加密、DOM、身份、集合、单例、文本、URL 和版本比较 | `settings`、DB/SystemConfig、网络请求、运行日志、MoviePilot 业务规则、旧导入路径 | `reflection.py`, `crypto.py`, `collections.py`, `text.py`, `url.py` | | `app/domain/` | Pure MoviePilot business semantics and models for media, recognition, sites, and torrents | Persistence, global settings reads, network/filesystem clients, Rust imports, service discovery, process lifecycle | `context.py`, `media.py`, `metainfo.py`, `scraper.py`, `meta/` | -| `app/runtime/` | 进程级运行机制和策略:配置、事件、完整日志、缓存契约/内存行为、托管资源门面、并发、调度、限流、本地化、GC 和重启状态 | 具体外部产品、业务流程、Redis/文件缓存实现 | `config.py`, `events.py`, `log.py`, `cache.py`, `managed_resources.py`, `thread.py`, `state.py` | -| `app/runtime/extensions/` | 模块、插件、配置化服务和托管资源实现的发现、注册与生命周期适配 | 通用反射机制、插件公开 API、无关业务流程 | `module_manager.py`, `plugin_manager.py`, `managed_resource_adapter.py`, `service_registry.py` | +| `app/runtime/` | 进程级运行机制和策略:配置、事件、完整日志、缓存契约/内存行为、托管资源门面、并发、调度、限流、本地化、GC 和重启状态 | 具体外部产品、业务流程、Redis/文件缓存实现 | `config.py`, `events.py`, `log.py`, `cache.py`, `resources.py`, `thread.py`, `state.py` | +| `app/runtime/extensions/` | 模块、插件、配置化服务和托管资源实现的发现、注册与生命周期适配 | 通用反射机制、插件公开 API、无关业务流程 | `module_manager.py`, `plugin_manager.py`, `resource.py`, `service_registry.py` | | `app/adapters/network/` | HTTP、浏览器、DNS、Cloudflare 和 IP 等通用网络技术适配 | RSS/站点业务编排、身份认证策略、命名外部产品流程 | `http.py`, `browser.py`, `doh.py`, `ip.py` | | `app/adapters/cache/` | Redis 与文件缓存等具体持久化实现 | 缓存协议、装饰器和进程内缓存策略 | `backends.py`, `redis.py` | | `app/adapters/system/` | 操作系统、文件、进程、标准流、包/资源安装、显示和 Rust 加速适配 | 业务规则、进程重启决策 | `host.py`, `display/`, `stdio.py`, `package.py`, `resource.py`, `rust.py`, `fsproxy.py` | diff --git a/app/adapters/network/browser.py b/app/adapters/network/browser.py index 8d9e473df..aae633c65 100644 --- a/app/adapters/network/browser.py +++ b/app/adapters/network/browser.py @@ -10,7 +10,7 @@ from urllib.parse import urlparse from app.adapters.network.http import RequestUtils, cookie_parse from app.runtime.log import logger -from app.runtime.managed_resources import ( +from app.runtime.resources import ( acquire_managed_resource, acquire_managed_resource_async, ) diff --git a/app/adapters/system/display/__init__.py b/app/adapters/system/display/__init__.py index f64aa0797..cc27999d6 100644 --- a/app/adapters/system/display/__init__.py +++ b/app/adapters/system/display/__init__.py @@ -7,12 +7,11 @@ from typing import Any from app.foundation.singleton import Singleton from app.runtime.log import logger -from app.runtime.managed_resources import ( +from app.runtime.resources import ( acquire_managed_resource, stop_managed_resource, ) - DISPLAY_CAPABILITY_ID = "host.display" diff --git a/app/agent/tools/impl/delete_transfer_history.py b/app/agent/tools/impl/delete_transfer_history.py index 3e43128ac..b28d0213e 100644 --- a/app/agent/tools/impl/delete_transfer_history.py +++ b/app/agent/tools/impl/delete_transfer_history.py @@ -9,7 +9,7 @@ from app.agent.tools.base import MoviePilotTool from app.agent.tools.tags import ToolTag from app.application.agentdata import get_agent_transfer_history_port from app.application.chain.data import get_chain_transfer_execution_port -from app.application.transfer_execution import ( +from app.application.transfer.execution import ( TransferExecutionCommand, TransferRetryRequestResult, ) diff --git a/app/api/endpoints/history.py b/app/api/endpoints/history.py index 973feeaf4..8fa343014 100644 --- a/app/api/endpoints/history.py +++ b/app/api/endpoints/history.py @@ -35,7 +35,7 @@ from app.application.history import ( HistoryQueryService, TransferHistoryMutationCommand, ) -from app.application.transfer_execution import ( +from app.application.transfer.execution import ( TransferExecutionCommand, TransferRetryRequestResult, ) diff --git a/app/api/endpoints/transfer.py b/app/api/endpoints/transfer.py index 1caf9314c..ba6575e86 100644 --- a/app/api/endpoints/transfer.py +++ b/app/api/endpoints/transfer.py @@ -11,7 +11,7 @@ from app.application.chain.data import get_chain_transfer_execution_port from app.application.configuration import get_api_runtime_config_snapshot from app.application.directory import DirectoryHelper from app.application.history import TransferHistoryLookupService -from app.application.transfer_execution import ( +from app.application.transfer.execution import ( TransferExecutionCommand, TransferExecutionConflictError, TransferExecutionState, diff --git a/app/application/chain/context.py b/app/application/chain/context.py index 3f8e45986..465e0dcc6 100644 --- a/app/application/chain/context.py +++ b/app/application/chain/context.py @@ -7,7 +7,7 @@ from dataclasses import dataclass, field from typing import Any, Optional from app.application.chain.data import ChainDataPorts -from app.application.chain.durable_events import ChainDurableEventWriter +from app.application.chain.events import ChainDurableEventWriter from app.application.configuration import ChainRuntimeConfig from app.runtime.stop import StopState, runtime_stop_state diff --git a/app/application/chain/data.py b/app/application/chain/data.py index f5fd5addf..92c92ea75 100644 --- a/app/application/chain/data.py +++ b/app/application/chain/data.py @@ -10,9 +10,8 @@ from collections.abc import Callable from dataclasses import dataclass from typing import Any, Optional -from app.application.transfer import TransferAdmissionRepository -from app.application.transfer_execution import TransferExecutionRepository - +from app.application.transfer.execution import TransferExecutionRepository +from app.application.transfer.workflow import TransferAdmissionRepository OperFactory = Callable[[], Any] TransferAdmissionRepositoryFactory = Callable[[], TransferAdmissionRepository] diff --git a/app/application/chain/durable_events.py b/app/application/chain/events.py similarity index 96% rename from app/application/chain/durable_events.py rename to app/application/chain/events.py index 848f7337e..21300e4f8 100644 --- a/app/application/chain/durable_events.py +++ b/app/application/chain/events.py @@ -1,4 +1,4 @@ -"""Chain durable 事件的事务写端口与可重放 payload 转换。""" +"""Chain 持久事件的事务写端口与可重放 payload 转换。""" from __future__ import annotations @@ -11,7 +11,7 @@ from typing import Any, Protocol, cast from uuid import uuid4 from app.application.history import TransferHistoryRecord, TransferHistoryWriter -from app.application.transfer_execution import TransferSettlementResult +from app.application.transfer.execution import TransferSettlementResult from app.domain.context import Context, MediaInfo, MusicInfo, TorrentInfo from app.domain.meta.metabase import MetaBase from app.domain.meta.metamusic import MetaMusic @@ -108,7 +108,11 @@ def snapshot_download_added(payload: dict[str, Any]) -> dict[str, Any]: context = payload.get("context") return cast(dict[str, Any], _json_snapshot({ "hash": payload.get("hash"), - "context": context.to_dict() if isinstance(context, Context) else context, + "context": ( + cast(Callable[[], dict[str, Any]], context.to_dict)() + if isinstance(context, Context) + else context + ), "username": payload.get("username"), "downloader": payload.get("downloader"), "episodes": list(payload.get("episodes") or []), @@ -211,11 +215,11 @@ def _restore_context(payload: dict[str, Any]) -> Context: media_info=( _restore_media(media_payload) if isinstance(media_payload, dict) else None ), - torrent_info=( + torrent_info=cast(TorrentInfo, ( _restore_torrent(torrent_payload) if isinstance(torrent_payload, dict) else None - ), + )), media_recognize_fail_count=int( payload.get("media_recognize_fail_count") or 0 ), diff --git a/app/application/transfer/__init__.py b/app/application/transfer/__init__.py new file mode 100644 index 000000000..fec0373bc --- /dev/null +++ b/app/application/transfer/__init__.py @@ -0,0 +1 @@ +"""整理应用能力包,分离任务工作流与持久执行状态机职责。""" diff --git a/app/application/transfer_execution.py b/app/application/transfer/execution.py similarity index 100% rename from app/application/transfer_execution.py rename to app/application/transfer/execution.py diff --git a/app/application/transfer.py b/app/application/transfer/workflow.py similarity index 90% rename from app/application/transfer.py rename to app/application/transfer/workflow.py index 8e165a174..ffa962592 100644 --- a/app/application/transfer.py +++ b/app/application/transfer/workflow.py @@ -22,6 +22,7 @@ from dataclasses import dataclass, field from pathlib import Path from time import monotonic from typing import ( + TYPE_CHECKING, Any, Callable, Dict, @@ -31,27 +32,29 @@ from typing import ( Tuple, TypeAlias, Union, + cast, ) from pydantic import BaseModel, ConfigDict, PrivateAttr from app.adapters.system.host import SystemUtils from app.application.agent import get_prompt_manager, get_running_agent_manager -from app.application.transfer_execution import TransferExecutionCheckpoint +from app.application.transfer.execution import TransferExecutionCheckpoint from app.domain.context import MediaInfo, MusicInfo from app.domain.media import normalize_music_type from app.domain.meta.metabase import MetaBase from app.domain.meta.metamusic import MetaMusic from app.foundation import text as text_tools from app.runtime.log import logger +from app.schemas.context import MediaInfo as _SchemaMediaInfo +from app.schemas.context import MetaInfo as _SchemaMetaInfo from app.schemas.file import FileItem from app.schemas.history import DownloadHistory from app.schemas.media import OptionalMediaIdentityMixin, resolve_media_identity +from app.schemas.music import MusicInfo as _SchemaMusicInfo +from app.schemas.music import MusicMeta as _SchemaMusicMeta from app.schemas.system import TransferDirectoryConf from app.schemas.tmdb import TmdbEpisode -from app.schemas.transfer import MetaInfo as _SchemaMetaInfo -from app.schemas.transfer import MusicInfo as _SchemaMusicInfo -from app.schemas.transfer import MusicMeta as _SchemaMusicMeta from app.schemas.transfer import TransferInfo, TransferJob, TransferJobTask from app.schemas.types import ( MUSIC_ENTITY_ALBUM, @@ -60,9 +63,61 @@ from app.schemas.types import ( MediaType, ReplyMode, ) -from app.schemas.workflow import MediaInfo as _SchemaMediaInfo + +if TYPE_CHECKING: + class _ApplicationModel: + """描述当前模块依赖的最小 Pydantic 模型类型形状。""" + + def __init__(self, **data: Any) -> None: + """接受模型字段关键字参数。""" + raise NotImplementedError + + def model_dump(self, **kwargs: Any) -> dict[str, Any]: + """返回模型字段字典。""" + raise NotImplementedError +else: + _ApplicationModel = BaseModel JSONValue: TypeAlias = Union[None, bool, int, float, str, list["JSONValue"], dict[str, "JSONValue"]] +JobId: TypeAlias = tuple[object, ...] +FileKey: TypeAlias = tuple[str, str] + + +class _DictionarySerializable(Protocol): + """描述领域对象沿用的字典投影能力。""" + + def to_dict(self) -> dict[str, Any]: + """返回领域对象的字典投影。""" + + +def _domain_to_dict(value: object) -> dict[str, Any]: + """按领域对象既有 ``to_dict`` 合同生成字典投影。""" + return cast(_DictionarySerializable, value).to_dict() + + +def _job_tasks(job: TransferJob) -> list[TransferJobTask]: + """声明进程内作业始终使用已初始化的任务列表。""" + return cast(list[TransferJobTask], job.tasks) + + +def _job_task_fileitem(task: TransferJobTask) -> FileItem: + """声明进程内作业任务始终绑定源文件。""" + return cast(FileItem, task.fileitem) + + +def _job_task_size(task: TransferJobTask) -> int: + """按既有本地目录回退规则返回已完成任务的文件大小。""" + fileitem = _job_task_fileitem(task) + if fileitem.size is not None: + return fileitem.size + if fileitem.storage == "local": + return SystemUtils.get_directory_size(Path(cast(str, fileitem.path))) + return 0 + + +def _transfer_task_meta(task: "TransferTask") -> MetaBase: + """声明进入作业管理器的整理任务已经完成元数据解析。""" + return cast(MetaBase, task.meta) TRANSFER_ADMISSION_ACCEPTED = "accepted" TRANSFER_ADMISSION_PROVIDER_PENDING = "provider_pending" @@ -636,7 +691,7 @@ class TransferLeaseLostError(TransferPlanningStateError): """整理 worker 已失去持久租约、不得继续推进任务时抛出的错误。""" -class TransferTask(OptionalMediaIdentityMixin, BaseModel): +class TransferTask(OptionalMediaIdentityMixin, _ApplicationModel): """ 文件整理任务。 """ @@ -748,7 +803,7 @@ class TransferTask(OptionalMediaIdentityMixin, BaseModel): self._lease_owner = owner_id self._lease_token = lease_token - def to_dict(self): + def to_dict(self) -> dict[str, Any]: """ 返回字典。 @@ -758,13 +813,16 @@ class TransferTask(OptionalMediaIdentityMixin, BaseModel): """ dicts = vars(self).copy() dicts["fileitem"] = self.fileitem.model_dump() if self.fileitem else None - dicts["meta"] = self.meta.to_dict() if self.meta else None - dicts["mediainfo"] = self.mediainfo.to_dict() if self.mediainfo else None + dicts["meta"] = _domain_to_dict(self.meta) if self.meta else None + dicts["mediainfo"] = _domain_to_dict(self.mediainfo) if self.mediainfo else None dicts["target_directory"] = self.target_directory.model_dump() if self.target_directory else None return dicts -class TransferQueue(BaseModel): +TransferCallback: TypeAlias = Callable[[TransferTask, TransferInfo], tuple[bool, str]] + + +class TransferQueue(_ApplicationModel): """ 异步整理队列信息。 @@ -774,7 +832,7 @@ class TransferQueue(BaseModel): # 任务信息 task: Optional[TransferTask] = None # 回调函数 - callback: Optional[Callable] = None + callback: Optional[TransferCallback] = None # 整理结果 result: Optional[TransferInfo] = None @@ -904,7 +962,7 @@ class TransferQueueService: self._list_tasks = list_tasks self._expire_tasks = expire_tasks - def put(self, task: TransferTask, callback: Callable) -> bool: + def put(self, task: TransferTask, callback: TransferCallback) -> bool: """先持久化准入事实再入队;任何前置失败都撤销内存作业视图。""" if not task or not self._register_task(task): return False @@ -1124,15 +1182,15 @@ class JobManager: """ # 整理中的作业 - _job_view: Dict[Tuple, TransferJob] = {} + _job_view: Dict[JobId, TransferJob] = {} # 汇总季集清单 - _season_episodes: Dict[Tuple, List[int]] = {} + _season_episodes: Dict[JobId, List[int]] = {} # 记录从 meta 作业迁移到 media 作业的关系,用于清理提前失败后残留的 media 作业 - _meta_to_media_ids: Dict[Tuple, set[Tuple]] = {} + _meta_to_media_ids: Dict[JobId, set[JobId]] = {} # 记录任务最近一次状态心跳,供外部异步接管任务的失活检测使用 - _task_state_changed_at: Dict[Tuple[str, str], float] = {} + _task_state_changed_at: Dict[FileKey, float] = {} # 记录仍由主程序整理线程直接执行的任务,避免把阻塞中的本地任务误判为失活 - _active_executions: set[Tuple[str, str]] = set() + _active_executions: set[FileKey] = set() def __init__(self) -> None: """初始化当前进程内的整理作业状态。""" @@ -1143,15 +1201,18 @@ class JobManager: self._active_executions = set() @staticmethod - def __get_meta_id(meta: MetaBase = None, season: Optional[int] = None) -> Tuple: + def __get_meta_id( + meta: Optional[MetaBase] = None, + season: Optional[int] = None, + ) -> JobId: """ 获取元数据ID """ - return meta.name, season + return cast(MetaBase, meta).name, season @staticmethod def __get_media_id(media: Optional[Union[MediaInfo, MusicInfo]] = None, - season: Optional[int] = None) -> Tuple: + season: Optional[int] = None) -> JobId: """ 获取媒体ID;音乐额外区分实体类型,并为无远端ID的曲目构造稳定身份。 """ @@ -1204,18 +1265,19 @@ class JobManager: ) return fileitem.storage or "local", normalized_path - def __get_id(self, task: TransferTask = None) -> Tuple: + def __get_id(self, task: Optional[TransferTask] = None) -> JobId: """ 获取作业ID """ - if task.mediainfo: + resolved_task = cast(TransferTask, task) + meta = _transfer_task_meta(resolved_task) + if resolved_task.mediainfo: return self.__get_media_id( - media=task.mediainfo, season=task.meta.begin_season + media=resolved_task.mediainfo, season=meta.begin_season ) - else: - return self.__get_meta_id(meta=task.meta, season=task.meta.begin_season) + return self.__get_meta_id(meta=meta, season=meta.begin_season) - def get_job_id(self, task: TransferTask) -> Tuple: + def get_job_id(self, task: TransferTask) -> JobId: """返回任务当前所属的稳定作业身份,供作业级附加状态隔离使用。""" return self.__get_id(task) @@ -1229,11 +1291,11 @@ class JobManager: mediainfo = deepcopy(task.mediainfo) mediainfo.clear() if isinstance(mediainfo, MusicInfo): - return _SchemaMusicInfo(**mediainfo.to_dict()) - return _SchemaMediaInfo(**mediainfo.to_dict()) + return _SchemaMusicInfo(**_domain_to_dict(mediainfo)) + return _SchemaMediaInfo(**_domain_to_dict(mediainfo)) else: # 没有媒体信息 - meta: MetaBase = task.meta + meta = _transfer_task_meta(task) if isinstance(meta, MetaMusic): # 未识别的音乐按已解析元数据兜底展示;音乐年份为 int, # 不能复用 MediaInfo(year 为 str),否则触发 pydantic 校验异常 @@ -1262,7 +1324,7 @@ class JobManager: """ if isinstance(task.meta, MetaMusic): return _SchemaMusicMeta(**task.meta.to_dict()) - return _SchemaMetaInfo(**task.meta.to_dict()) + return _SchemaMetaInfo(**_domain_to_dict(_transfer_task_meta(task))) def add_task(self, task: TransferTask, state: Optional[str] = "waiting") -> bool: """ @@ -1278,16 +1340,16 @@ class JobManager: __mediaid__ = self.__get_id(task) # 同一个源文件可能在识别前后落入不同作业,必须跨作业去重。 if any( - self.__get_file_key(t.fileitem) == file_key + self.__get_file_key(_job_task_fileitem(t)) == file_key for job in self._job_view.values() - for t in job.tasks + for t in _job_tasks(job) ): logger.debug(f"任务 {task.fileitem.name} 已存在,跳过重复添加") return False if __mediaid__ not in self._job_view: self._job_view[__mediaid__] = TransferJob( media=self.__get_media(task), - season=task.meta.begin_season, + season=_transfer_task_meta(task).begin_season, tasks=[ TransferJobTask( fileitem=task.fileitem, @@ -1302,13 +1364,13 @@ class JobManager: # 不重复添加任务 if any( [ - self.__get_file_key(t.fileitem) == file_key - for t in self._job_view[__mediaid__].tasks + self.__get_file_key(_job_task_fileitem(t)) == file_key + for t in _job_tasks(self._job_view[__mediaid__]) ] ): logger.debug(f"任务 {task.fileitem.name} 已存在,跳过重复添加") return False - self._job_view[__mediaid__].tasks.append( + _job_tasks(self._job_view[__mediaid__]).append( TransferJobTask( fileitem=task.fileitem, meta=self.__get_meta(task), @@ -1320,12 +1382,14 @@ class JobManager: self._task_state_changed_at[file_key] = monotonic() # 添加季集信息 if self._season_episodes.get(__mediaid__): - self._season_episodes[__mediaid__].extend(task.meta.episode_list) + self._season_episodes[__mediaid__].extend( + _transfer_task_meta(task).episode_list + ) self._season_episodes[__mediaid__] = list( set(self._season_episodes[__mediaid__]) ) else: - self._season_episodes[__mediaid__] = task.meta.episode_list + self._season_episodes[__mediaid__] = _transfer_task_meta(task).episode_list return True def migrate_task(self, task: TransferTask) -> bool: @@ -1338,16 +1402,15 @@ class JobManager: if not self.add_task(task, state=curr_task.state if curr_task else "waiting"): return False if curr_task and task.mediainfo: - metaid = self.__get_meta_id( - meta=task.meta, season=task.meta.begin_season - ) + meta = _transfer_task_meta(task) + metaid = self.__get_meta_id(meta=meta, season=meta.begin_season) mediaid = self.__get_id(task) if source_job_id == metaid and mediaid != metaid: with job_lock: self._meta_to_media_ids.setdefault(metaid, set()).add(mediaid) return True - def __is_job_done(self, job_id: Tuple) -> bool: + def __is_job_done(self, job_id: JobId) -> bool: """ 检查指定作业是否已完成 """ @@ -1355,10 +1418,10 @@ class JobManager: return True return all( task.state in ["completed", "failed"] - for task in self._job_view[job_id].tasks + for task in _job_tasks(self._job_view[job_id]) ) - def __pop_job(self, job_id: Tuple): + def __pop_job(self, job_id: JobId) -> None: """ 移除指定作业和对应季集缓存 """ @@ -1366,13 +1429,13 @@ class JobManager: self._season_episodes.pop(job_id, None) if not job: return - for task in job.tasks: - file_key = self.__get_file_key(task.fileitem) + for task in _job_tasks(job): + file_key = self.__get_file_key(_job_task_fileitem(task)) if file_key: self._task_state_changed_at.pop(file_key, None) self._active_executions.discard(file_key) - def __remove_done_job_groups(self, job_ids: set[Tuple]): + def __remove_done_job_groups(self, job_ids: set[JobId]) -> None: """ 清理已进入终态的独立作业或关联作业组。 """ @@ -1396,7 +1459,7 @@ class JobManager: if self.__is_job_done(job_id): self.__pop_job(job_id) - def start_execution(self, task: TransferTask): + def start_execution(self, task: TransferTask) -> None: """ 标记任务仍由主程序整理线程直接执行。 @@ -1410,7 +1473,7 @@ class JobManager: with job_lock: self._active_executions.add(file_key) - def finish_execution(self, task: TransferTask): + def finish_execution(self, task: TransferTask) -> None: """ 结束主程序整理线程对任务的直接执行标记。 @@ -1426,7 +1489,7 @@ class JobManager: def expire_stale_running_tasks( self, timeout_seconds: int - ) -> List[Tuple[FileItem, int]]: + ) -> List[tuple[FileItem, int]]: """ 将外部接管后长期无心跳的运行中任务标记失败并清理作业视图。 @@ -1440,12 +1503,13 @@ class JobManager: return [] current_time = monotonic() - expired: List[Tuple[FileItem, int]] = [] - affected_job_ids: set[Tuple] = set() + expired: List[tuple[FileItem, int]] = [] + affected_job_ids: set[JobId] = set() with job_lock: for mediaid, job in self._job_view.items(): - for task in job.tasks: - file_key = self.__get_file_key(task.fileitem) + for task in _job_tasks(job): + fileitem = _job_task_fileitem(task) + file_key = self.__get_file_key(fileitem) if ( not file_key or task.state != "running" @@ -1463,13 +1527,13 @@ class JobManager: self._season_episodes[mediaid] = list( set(self._season_episodes[mediaid]) - set(episodes) ) - expired.append((task.fileitem, int(inactive_seconds))) + expired.append((fileitem, int(inactive_seconds))) affected_job_ids.add(mediaid) self.__remove_done_job_groups(affected_job_ids) return expired - def running_task(self, task: TransferTask): + def running_task(self, task: TransferTask) -> None: """ 设置任务为运行中,并刷新外部异步任务的状态心跳。 """ @@ -1478,15 +1542,15 @@ class JobManager: if __mediaid__ not in self._job_view: return # 更新状态 - for t in self._job_view[__mediaid__].tasks: + for t in _job_tasks(self._job_view[__mediaid__]): if t.fileitem == task.fileitem: t.state = "running" - file_key = self.__get_file_key(t.fileitem) + file_key = self.__get_file_key(_job_task_fileitem(t)) if file_key: self._task_state_changed_at[file_key] = monotonic() break - def finish_task(self, task: TransferTask): + def finish_task(self, task: TransferTask) -> None: """ 设置任务为完成/成功 """ @@ -1495,15 +1559,15 @@ class JobManager: if __mediaid__ not in self._job_view: return # 更新状态 - for t in self._job_view[__mediaid__].tasks: + for t in _job_tasks(self._job_view[__mediaid__]): if t.fileitem == task.fileitem: t.state = "completed" - file_key = self.__get_file_key(t.fileitem) + file_key = self.__get_file_key(_job_task_fileitem(t)) if file_key: self._task_state_changed_at[file_key] = monotonic() break - def fail_task(self, task: TransferTask): + def fail_task(self, task: TransferTask) -> None: """ 设置任务为失败 """ @@ -1512,10 +1576,10 @@ class JobManager: if __mediaid__ not in self._job_view: return # 更新状态 - for t in self._job_view[__mediaid__].tasks: + for t in _job_tasks(self._job_view[__mediaid__]): if t.fileitem == task.fileitem: t.state = "failed" - file_key = self.__get_file_key(t.fileitem) + file_key = self.__get_file_key(_job_task_fileitem(t)) if file_key: self._task_state_changed_at[file_key] = monotonic() break @@ -1523,10 +1587,10 @@ class JobManager: if __mediaid__ in self._season_episodes: self._season_episodes[__mediaid__] = list( set(self._season_episodes[__mediaid__]) - - set(task.meta.episode_list) + - set(_transfer_task_meta(task).episode_list) ) - def fail_unfinished_task(self, task: TransferTask): + def fail_unfinished_task(self, task: TransferTask) -> None: """ 将指定任务视图中的非终态任务标记为失败 """ @@ -1537,8 +1601,8 @@ class JobManager: return with job_lock: for mediaid, job in self._job_view.items(): - for job_task in job.tasks: - if self.__get_file_key(job_task.fileitem) != file_key: + for job_task in _job_tasks(job): + if self.__get_file_key(_job_task_fileitem(job_task)) != file_key: continue if job_task.state not in ["completed", "failed"]: job_task.state = "failed" @@ -1546,7 +1610,7 @@ class JobManager: if mediaid in self._season_episodes: self._season_episodes[mediaid] = list( set(self._season_episodes[mediaid]) - - set(task.meta.episode_list) + - set(_transfer_task_meta(task).episode_list) ) return @@ -1561,7 +1625,7 @@ class JobManager: self, fileitem: FileItem, preserve_execution: bool = False, - ) -> Tuple[Optional[TransferJobTask], Optional[Tuple]]: + ) -> tuple[Optional[TransferJobTask], Optional[JobId]]: """ 根据文件项移除任务,并返回任务所在的作业ID """ @@ -1571,14 +1635,14 @@ class JobManager: with job_lock: for mediaid in list(self._job_view): job = self._job_view[mediaid] - for task in job.tasks: - if self.__get_file_key(task.fileitem) == file_key: - job.tasks.remove(task) + for task in _job_tasks(job): + if self.__get_file_key(_job_task_fileitem(task)) == file_key: + _job_tasks(job).remove(task) self._task_state_changed_at.pop(file_key, None) if not preserve_execution: self._active_executions.discard(file_key) # 如果没有作业了,则移除作业 - if not job.tasks: + if not _job_tasks(job): self._job_view.pop(mediaid) # 移除季集信息 if mediaid in self._season_episodes: @@ -1602,16 +1666,15 @@ class JobManager: return job return None - def try_remove_job(self, task: TransferTask): + def try_remove_job(self, task: TransferTask) -> None: """ 尝试移除任务对应的作业(严格检查未完成作业,线程安全) """ with job_lock: - __metaid__ = self.__get_meta_id( - meta=task.meta, season=task.meta.begin_season - ) + meta = _transfer_task_meta(task) + __metaid__ = self.__get_meta_id(meta=meta, season=meta.begin_season) __mediaid__ = self.__get_media_id( - media=task.mediainfo, season=task.meta.begin_season + media=task.mediainfo, season=meta.begin_season ) related_media_ids = set(self._meta_to_media_ids.get(__metaid__, set())) @@ -1634,23 +1697,22 @@ class JobManager: 检查任务对应的作业是否整理完成(不管成功还是失败) """ with job_lock: - __metaid__ = self.__get_meta_id( - meta=task.meta, season=task.meta.begin_season - ) + meta = _transfer_task_meta(task) + __metaid__ = self.__get_meta_id(meta=meta, season=meta.begin_season) __mediaid__ = self.__get_media_id( - media=task.mediainfo, season=task.meta.begin_season + media=task.mediainfo, season=meta.begin_season ) if __metaid__ in self._job_view: meta_done = all( task.state in ["completed", "failed"] - for task in self._job_view[__metaid__].tasks + for task in _job_tasks(self._job_view[__metaid__]) ) else: meta_done = True if __mediaid__ in self._job_view: media_done = all( task.state in ["completed", "failed"] - for task in self._job_view[__mediaid__].tasks + for task in _job_tasks(self._job_view[__mediaid__]) ) else: media_done = True @@ -1661,21 +1723,20 @@ class JobManager: 检查任务对应的作业是否已完成且有成功的记录 """ with job_lock: - __metaid__ = self.__get_meta_id( - meta=task.meta, season=task.meta.begin_season - ) + meta = _transfer_task_meta(task) + __metaid__ = self.__get_meta_id(meta=meta, season=meta.begin_season) __mediaid__ = self.__get_media_id( - media=task.mediainfo, season=task.meta.begin_season + media=task.mediainfo, season=meta.begin_season ) if __metaid__ in self._job_view: meta_finished = all( task.state in ["completed", "failed"] - for task in self._job_view[__metaid__].tasks + for task in _job_tasks(self._job_view[__metaid__]) ) else: meta_finished = True if __mediaid__ in self._job_view: - tasks = self._job_view[__mediaid__].tasks + tasks = _job_tasks(self._job_view[__mediaid__]) media_finished = all( task.state in ["completed", "failed"] for task in tasks ) and any(task.state == "completed" for task in tasks) @@ -1688,23 +1749,22 @@ class JobManager: 检查任务对应的作业是否全部成功 """ with job_lock: - __metaid__ = self.__get_meta_id( - meta=task.meta, season=task.meta.begin_season - ) + meta = _transfer_task_meta(task) + __metaid__ = self.__get_meta_id(meta=meta, season=meta.begin_season) __mediaid__ = self.__get_media_id( - media=task.mediainfo, season=task.meta.begin_season + media=task.mediainfo, season=meta.begin_season ) if __metaid__ in self._job_view: meta_success = all( task.state in ["completed"] - for task in self._job_view[__metaid__].tasks + for task in _job_tasks(self._job_view[__metaid__]) ) else: meta_success = True if __mediaid__ in self._job_view: media_success = all( task.state in ["completed"] - for task in self._job_view[__mediaid__].tasks + for task in _job_tasks(self._job_view[__mediaid__]) ) else: media_success = True @@ -1716,9 +1776,9 @@ class JobManager: """ with job_lock: return { - task.download_hash + cast(str, task.download_hash) for job in self._job_view.values() - for task in job.tasks + for task in _job_tasks(job) } def is_torrent_done(self, download_hash: str) -> bool: @@ -1729,7 +1789,7 @@ class JobManager: if any( task.state not in {"completed", "failed"} for job in self._job_view.values() - for task in job.tasks + for task in _job_tasks(job) if task.download_hash == download_hash ): return False @@ -1743,7 +1803,7 @@ class JobManager: if any( task.state != "completed" for job in self._job_view.values() - for task in job.tasks + for task in _job_tasks(job) if task.download_hash == download_hash ): return False @@ -1767,7 +1827,7 @@ class JobManager: __metaid__ = self.__get_meta_id(meta=meta, season=season) return ( __metaid__ in self._job_view - and len(self._job_view[__metaid__].tasks) > 0 + and len(_job_tasks(self._job_view[__metaid__])) > 0 ) def success_tasks( @@ -1782,7 +1842,7 @@ class JobManager: return [] return [ task - for task in self._job_view[__mediaid__].tasks + for task in _job_tasks(self._job_view[__mediaid__]) if task.state == "completed" ] @@ -1796,7 +1856,7 @@ class JobManager: __mediaid__ = self.__get_media_id(media=media, season=season) if __mediaid__ not in self._job_view: return [] - return self._job_view[__mediaid__].tasks + return _job_tasks(self._job_view[__mediaid__]) def count(self, media: Union[MediaInfo, MusicInfo], season: Optional[int] = None) -> int: """ @@ -1809,7 +1869,7 @@ class JobManager: return len( [ task - for task in self._job_view[__mediaid__].tasks + for task in _job_tasks(self._job_view[__mediaid__]) if task.state == "completed" ] ) @@ -1824,14 +1884,8 @@ class JobManager: return 0 return sum( [ - task.fileitem.size - if task.fileitem.size is not None - else ( - SystemUtils.get_directory_size(Path(task.fileitem.path)) - if task.fileitem.storage == "local" - else 0 - ) - for task in self._job_view[__mediaid__].tasks + _job_task_size(task) + for task in _job_tasks(self._job_view[__mediaid__]) if task.state == "completed" ] ) @@ -1841,7 +1895,7 @@ class JobManager: 获取所有任务总数 """ with job_lock: - return sum([len(job.tasks) for job in self._job_view.values()]) + return sum([len(_job_tasks(job)) for job in self._job_view.values()]) def pending_total(self) -> int: """ @@ -1855,7 +1909,7 @@ class JobManager: return sum( 1 for job in self._job_view.values() - for task in job.tasks + for task in _job_tasks(job) if task.state not in ("completed", "failed") ) @@ -1960,10 +2014,10 @@ class FailedRetryScheduler: def _build_retry_transfer_prompt(self, history_ids: list[int]) -> str: """根据失败记录数量构建统一的重试整理后台任务提示词。""" task_type, template_context = self._build_retry_transfer_template_context(history_ids) - return get_prompt_manager().render_system_task_message( + return cast(str, get_prompt_manager().render_system_task_message( task_type, template_context=template_context, - ) + )) async def schedule_retry(self, history_id: int, group_key: str = "") -> None: """ diff --git a/app/chain/__init__.py b/app/chain/__init__.py index b2161c652..b6ccd5315 100644 --- a/app/chain/__init__.py +++ b/app/chain/__init__.py @@ -36,7 +36,7 @@ from app.schemas.types import ( from app.schemas.workflow import FileItem if TYPE_CHECKING: - from app.application.transfer import TransferPlanCheckpoint, TransferPlanningInput + from app.application.transfer.workflow import TransferPlanCheckpoint, TransferPlanningInput class ChainBase(RecognitionMixin, MessageProcessingMixin, NotificationMixin, diff --git a/app/chain/_transfer.py b/app/chain/_transfer.py index c6a8eb695..546a54345 100644 --- a/app/chain/_transfer.py +++ b/app/chain/_transfer.py @@ -14,7 +14,6 @@ from typing import Any, Callable, Dict, List, Optional, Tuple, Union, cast from app.adapters.system.host import SystemUtils from app.application.agent import build_manual_redo_prompt, get_running_agent_manager from app.application.chain.data import ( - get_chain_download_history_port, get_chain_transfer_execution_port, get_chain_transfer_history_port, ) @@ -24,17 +23,12 @@ from app.application.configuration import ( ) from app.application.formatting import EpisodeFormatRuleHelper from app.application.history import clear_transfer_failures, resolve_history -from app.application.transfer import TransferTask, job_lock -from app.application.transfer_execution import TransferExecutionCommand +from app.application.transfer.execution import TransferExecutionCommand +from app.application.transfer.workflow import TransferTask, job_lock from app.chain._contracts import TransferMixinHost from app.chain.media import MediaChain from app.chain.storage import StorageChain from app.chain.subscribe import SubscribeChain - -# 旧测试与插件补丁入口;正式依赖通过宿主工厂逐步收敛。 -MediaChain = MediaChain -StorageChain = StorageChain -SubscribeChain = SubscribeChain from app.domain.context import MediaInfo, MusicInfo from app.domain.media import normalize_music_type from app.domain.meta.metabase import MetaBase diff --git a/app/chain/transfer.py b/app/chain/transfer.py index 636a9a56e..9621e3b40 100755 --- a/app/chain/transfer.py +++ b/app/chain/transfer.py @@ -24,7 +24,7 @@ from app.application.chain.data import ( get_chain_transfer_history_port, get_chain_transfer_pending_port, ) -from app.application.chain.durable_events import TransferResultSettlement +from app.application.chain.events import TransferResultSettlement from app.application.configuration import get_configured_system_config from app.application.directory import DirectoryHelper from app.application.formatting import FormatParser @@ -46,7 +46,21 @@ from app.application.outbox import ( TRANSFER_COMPLETED_TOPIC, TRANSFER_FAILED_TOPIC, ) -from app.application.transfer import ( +from app.application.transfer.execution import ( + TransferExecutionCheckpoint, + TransferExecutionCommand, + TransferExecutionConflictError, + TransferExecutionRepository, + TransferExecutionSnapshot, + TransferExecutionState, + TransferOperationObservation, + TransferOperationObservationState, + TransferSettlementResult, + TransferStepIntent, + TransferStepResult, + TransferStepState, +) +from app.application.transfer.workflow import ( FailedRetryScheduler, JobManager, TransferAdmission, @@ -64,20 +78,6 @@ from app.application.transfer import ( build_transfer_failure_group_key, job_lock, ) -from app.application.transfer_execution import ( - TransferExecutionCheckpoint, - TransferExecutionCommand, - TransferExecutionConflictError, - TransferExecutionRepository, - TransferExecutionSnapshot, - TransferExecutionState, - TransferOperationObservation, - TransferOperationObservationState, - TransferSettlementResult, - TransferStepIntent, - TransferStepResult, - TransferStepState, -) from app.chain import ChainBase from app.chain._transfer import ( EpisodeFormatMixin, diff --git a/app/db/adapters/chain.py b/app/db/adapters/chain.py index ee422b13d..639b8a2d0 100644 --- a/app/db/adapters/chain.py +++ b/app/db/adapters/chain.py @@ -10,7 +10,7 @@ from typing import Any from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session -from app.application.chain.durable_events import ( +from app.application.chain.events import ( ChainDurableEventWriter, TransferHistoryRef, TransferResultSettlement, @@ -25,7 +25,7 @@ from app.application.outbox import ( DurableEventCommand, OutboxIntent, ) -from app.application.transfer_execution import ( +from app.application.transfer.execution import ( TransferExecutionConflictError, TransferExecutionLeaseLostError, TransferExecutionState, diff --git a/app/db/adapters/transfer/__init__.py b/app/db/adapters/transfer/__init__.py new file mode 100644 index 000000000..70ed86a95 --- /dev/null +++ b/app/db/adapters/transfer/__init__.py @@ -0,0 +1 @@ +"""转移接纳与持久执行的数据库适配器包。""" diff --git a/app/db/adapters/transfer.py b/app/db/adapters/transfer/admission.py similarity index 99% rename from app/db/adapters/transfer.py rename to app/db/adapters/transfer/admission.py index 5cac4dd46..61fb5c854 100644 --- a/app/db/adapters/transfer.py +++ b/app/db/adapters/transfer/admission.py @@ -10,7 +10,7 @@ from uuid import uuid4 from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session -from app.application.transfer import ( +from app.application.transfer.workflow import ( TRANSFER_ADMISSION_ACCEPTED, TRANSFER_ADMISSION_PLANNED, TRANSFER_ADMISSION_PROVIDER_PENDING, diff --git a/app/db/adapters/transfer_execution.py b/app/db/adapters/transfer/execution.py similarity index 99% rename from app/db/adapters/transfer_execution.py rename to app/db/adapters/transfer/execution.py index f8a34793c..c81ff9d2e 100644 --- a/app/db/adapters/transfer_execution.py +++ b/app/db/adapters/transfer/execution.py @@ -10,7 +10,7 @@ from sqlalchemy import and_, func, or_, select from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import Session -from app.application.transfer_execution import ( +from app.application.transfer.execution import ( TransferExecutionCheckpoint, TransferExecutionConflictError, TransferExecutionLeaseLostError, diff --git a/app/modules/filemanager/module.py b/app/modules/filemanager/module.py index 2d3585563..fd9c5f05b 100644 --- a/app/modules/filemanager/module.py +++ b/app/modules/filemanager/module.py @@ -4,8 +4,8 @@ from typing import Any, Callable, Dict, List, Optional, Tuple, Union from app.adapters.system.host import SystemUtils from app.application.directory import DirectoryHelper from app.application.messaging.message import MessageHelper -from app.application.transfer import TransferPlanCheckpoint, TransferPlanningInput -from app.application.transfer_execution import TransferStepRunner +from app.application.transfer.execution import TransferStepRunner +from app.application.transfer.workflow import TransferPlanCheckpoint, TransferPlanningInput from app.domain.context import MediaInfo, MusicInfo from app.domain.meta.metabase import MetaBase from app.domain.meta.metamusic import MetaMusic diff --git a/app/modules/filemanager/transhandler.py b/app/modules/filemanager/transhandler.py index 61a41c5d9..579c13fc6 100644 --- a/app/modules/filemanager/transhandler.py +++ b/app/modules/filemanager/transhandler.py @@ -10,17 +10,17 @@ from app.adapters.system.host import SystemUtils from app.application.audio import AudioMetadataHelper from app.application.directory import DirectoryHelper from app.application.messaging.message import TemplateHelper -from app.application.transfer import ( - TransferPlanCheckpoint, - TransferPlanItem, - TransferPlanningInput, -) -from app.application.transfer_execution import ( +from app.application.transfer.execution import ( TransferOperationObservation, TransferOperationObservationState, TransferStepResult, TransferStepRunner, ) +from app.application.transfer.workflow import ( + TransferPlanCheckpoint, + TransferPlanItem, + TransferPlanningInput, +) from app.domain.context import MediaInfo, MusicInfo from app.domain.meta.metabase import MetaBase from app.domain.meta.metamusic import MetaMusic diff --git a/app/runtime/compat/manifest.py b/app/runtime/compat/manifest.py index 535439b51..2d05c3d79 100644 --- a/app/runtime/compat/manifest.py +++ b/app/runtime/compat/manifest.py @@ -56,6 +56,24 @@ MODULE_ALIASES: Dict[str, ModuleAlias] = { introduced="v3.0.0", owner="application", ), + "app.application.chain.durable_events": ModuleAlias( + target="app.application.chain.events", + replacement="app.application.chain.events", + introduced="v3.0.0", + owner="application", + ), + "app.application.transfer_execution": ModuleAlias( + target="app.application.transfer.execution", + replacement="app.application.transfer.execution", + introduced="v3.0.0", + owner="application", + ), + "app.runtime.managed_resources": ModuleAlias( + target="app.runtime.resources", + replacement="app.runtime.resources", + introduced="v3.0.0", + owner="runtime", + ), "app.db.agentchat_oper": ModuleAlias( target="app.db.oper.agentchat", replacement="app.db.oper.agentchat", @@ -136,7 +154,7 @@ MODULE_ALIASES: Dict[str, ModuleAlias] = { ), "app.db.transferpending_oper": ModuleAlias( target="app.sdk._legacy.transferpending", - replacement="app.application.transfer", + replacement="app.application.transfer.workflow", introduced="v3.0.0", owner="sdk", ), @@ -756,6 +774,14 @@ _MESSAGE_NOTIFICATION_SYMBOL_ALIASES: Dict[str, SymbolAlias] = { } SYMBOL_ALIASES: Dict[str, Dict[str, SymbolAlias]] = { + "app.application.transfer": { + name: SymbolAlias( + target_module="app.sdk._legacy.transfer", + target_name=name, + replacement=f"app.application.transfer.workflow.{name}", + ) + for name in ("TransferTask", "TransferQueue") + }, "app.agent.orchestrator": { "AgentChain": SymbolAlias( target_module="app.chain.agent", @@ -806,7 +832,7 @@ SYMBOL_ALIASES: Dict[str, Dict[str, SymbolAlias]] = { name: SymbolAlias( target_module="app.sdk._legacy.transfer", target_name=name, - replacement=f"app.application.transfer.{name}", + replacement=f"app.application.transfer.workflow.{name}", ) for name in ("TransferTask", "TransferQueue") }, @@ -817,7 +843,7 @@ SYMBOL_ALIASES: Dict[str, Dict[str, SymbolAlias]] = { name: SymbolAlias( target_module="app.sdk._legacy.transfer", target_name=name, - replacement=f"app.application.transfer.{name}", + replacement=f"app.application.transfer.workflow.{name}", ) for name in ("TransferTask", "TransferQueue") }, diff --git a/app/runtime/extensions/managed_resource_adapter.py b/app/runtime/extensions/resource.py similarity index 99% rename from app/runtime/extensions/managed_resource_adapter.py rename to app/runtime/extensions/resource.py index 86c9e5d47..20d5d0458 100644 --- a/app/runtime/extensions/managed_resource_adapter.py +++ b/app/runtime/extensions/resource.py @@ -15,12 +15,11 @@ from app.runtime.capabilities.model import ( CapabilitySpec, ) from app.runtime.capabilities.registry import CapabilityRegistry -from app.runtime.managed_resources import ( +from app.runtime.resources import ( MANAGED_RESOURCE_ASYNC_KIND, MANAGED_RESOURCE_SYNC_KIND, ) - _DEFAULT_RESOURCE_ROOT = Path(__file__).resolve().parents[2] / "adapters" _RESOURCE_KINDS = {MANAGED_RESOURCE_SYNC_KIND, MANAGED_RESOURCE_ASYNC_KIND} diff --git a/app/runtime/managed_resources.py b/app/runtime/resources.py similarity index 96% rename from app/runtime/managed_resources.py rename to app/runtime/resources.py index 2f3dcab93..73c95aa5a 100644 --- a/app/runtime/managed_resources.py +++ b/app/runtime/resources.py @@ -4,8 +4,7 @@ from __future__ import annotations import asyncio import threading -from typing import Any, Optional, Protocol - +from typing import Any, Literal, Optional, Protocol, overload MANAGED_RESOURCE_SYNC_KIND = "managed_resource.sync" MANAGED_RESOURCE_ASYNC_KIND = "managed_resource.async" @@ -65,6 +64,14 @@ def configure_managed_resource_runtime(runtime: ManagedResourceRuntime) -> None: _managed_resource_runtime = runtime +@overload +def _runtime(*, required: Literal[True]) -> ManagedResourceRuntime: ... + + +@overload +def _runtime(*, required: Literal[False]) -> Optional[ManagedResourceRuntime]: ... + + def _runtime(*, required: bool) -> Optional[ManagedResourceRuntime]: """读取当前 Runtime;资源使用路径要求启动组合已经完成装配。""" with _runtime_lock: diff --git a/app/schemas/transfer.py b/app/schemas/transfer.py index 5065dca3c..581048633 100644 --- a/app/schemas/transfer.py +++ b/app/schemas/transfer.py @@ -1,15 +1,14 @@ from pathlib import Path -from typing import Literal, List, Optional, Union +from typing import List, Literal, Optional, Union from pydantic import BaseModel, ConfigDict, Field, model_validator from app.schemas.common import JsonData -from app.schemas.media import OptionalMediaIdentityMixin -from app.schemas.types import MediaSource, MusicTargetEntityType - -from app.schemas.context import MetaInfo, MediaInfo -from app.schemas.music import MusicInfo, MusicMeta +from app.schemas.context import MediaInfo, MetaInfo from app.schemas.file import FileItem +from app.schemas.media import OptionalMediaIdentityMixin +from app.schemas.music import MusicInfo, MusicMeta +from app.schemas.types import MediaSource, MusicTargetEntityType class DownloaderTorrent(BaseModel): @@ -88,7 +87,7 @@ class DownloadingTorrent(DownloaderTorrent): """ -# TransferTask 已迁至 app/application/transfer.py:它是整理链的进程内工作项,装的是 +# TransferTask 已迁至 app/application/transfer/workflow.py:它是整理链的进程内工作项,装的是 # 领域对象而非 DTO,留在这里只能把两个字段标成 Any——app.schemas 命名领域类型会让 # app.schemas -> app.schemas.transfer -> app.domain.* -> app.schemas.types -> app.schemas # 闭环。下面的 TransferJob / TransferJobTask 才是它面向前端的投影,用本包的同名 DTO。 diff --git a/app/sdk/_legacy/transfer.py b/app/sdk/_legacy/transfer.py index e617209d5..24992e560 100644 --- a/app/sdk/_legacy/transfer.py +++ b/app/sdk/_legacy/transfer.py @@ -2,8 +2,10 @@ from typing import Any, Optional -from app.application.transfer import ( +from app.application.transfer.workflow import ( TransferQueue as CanonicalTransferQueue, +) +from app.application.transfer.workflow import ( TransferTask as CanonicalTransferTask, ) diff --git a/app/startup/initializers/modules.py b/app/startup/initializers/modules.py index eee8d49e4..9ebe1256b 100644 --- a/app/startup/initializers/modules.py +++ b/app/startup/initializers/modules.py @@ -44,7 +44,7 @@ from app.application.chain.context import ( configure_chain_runtime_context_provider, ) from app.application.chain.data import configure_chain_data_ports, get_chain_data_ports -from app.application.chain.durable_events import ( +from app.application.chain.events import ( restore_download_added, restore_transfer_result, ) @@ -110,8 +110,8 @@ from app.db.adapters.outbox import SqlAlchemyAsyncOutboxStager, SqlAlchemyOutbox from app.db.adapters.site import TransactionalSiteRepository from app.db.adapters.subscription import TransactionalSubscribeWriter from app.db.adapters.transaction import TransactionalWriteRunner -from app.db.adapters.transfer import TransactionalTransferAdmissionRepository -from app.db.adapters.transfer_execution import ( +from app.db.adapters.transfer.admission import TransactionalTransferAdmissionRepository +from app.db.adapters.transfer.execution import ( TransactionalTransferExecutionRepository, ) from app.db.adapters.workflow import TransactionalWorkflowExecutionService @@ -189,7 +189,7 @@ from app.startup.composition.subscription import ( configure_transactional_subscription_scopes, ) from app.startup.initializers.agent import init_agent -from app.startup.initializers.managed_resources import ( +from app.startup.initializers.resources import ( init_managed_resources, stop_managed_resources, ) diff --git a/app/startup/initializers/plugins.py b/app/startup/initializers/plugins.py index 8d7e6e9dc..dae08cc35 100644 --- a/app/startup/initializers/plugins.py +++ b/app/startup/initializers/plugins.py @@ -79,7 +79,7 @@ from app.runtime.extensions.plugin_manager import ( configure_site_auth_level_provider, ) from app.runtime.log import logger -from app.runtime.managed_resources import acquire_managed_resource +from app.runtime.resources import acquire_managed_resource from app.runtime.settings import get_runtime_setting from app.schemas.exception import PluginMutationRejectedError from app.schemas.plugin import PluginRuntimeStatus diff --git a/app/startup/initializers/managed_resources.py b/app/startup/initializers/resources.py similarity index 93% rename from app/startup/initializers/managed_resources.py rename to app/startup/initializers/resources.py index 1bb33d6e5..7902ee42d 100644 --- a/app/startup/initializers/managed_resources.py +++ b/app/startup/initializers/resources.py @@ -6,12 +6,12 @@ import threading from typing import Optional from app.runtime.capabilities.runtime import CapabilityRuntime -from app.runtime.extensions.managed_resource_adapter import ( +from app.runtime.extensions.resource import ( AsyncManagedResourceAdapter, SyncManagedResourceAdapter, build_managed_resource_registry, ) -from app.runtime.managed_resources import ( +from app.runtime.resources import ( MANAGED_RESOURCE_ASYNC_KIND, MANAGED_RESOURCE_SYNC_KIND, configure_managed_resource_runtime, diff --git a/docs/architecture-optimization-checklist.md b/docs/architecture-optimization-checklist.md index 41b26d173..edce89cbe 100644 --- a/docs/architecture-optimization-checklist.md +++ b/docs/architecture-optimization-checklist.md @@ -69,7 +69,7 @@ MoviePilot V3 已经形成较清晰的模块化单体:`foundation`、`domain` | 指标 | 当前值 | 解释 | |---|---:|---| -| 宿主 Python 模块 / 内部依赖边 | 842 / 6,882 | `dependency-baseline.json` 当前快照 | +| 宿主 Python 模块 / 内部依赖边 | 844 / 6,898 | `dependency-baseline.json` 当前快照 | | 非平凡 SCC | 2 | 新增 Chain 包根环;另一个是隔离的 29 模块 TMDB 移植包环 | | 跨层 DB 边界债务 | 0 | Application、Chain、API、Agent、Runtime、Workflow 到 DB 的受控债务均为零 | | Model/Oper 事务债务 | 0 | 自建 Session、自动事务装饰器、直接 commit/rollback 等基线均为零 | @@ -77,9 +77,9 @@ MoviePilot V3 已经形成较清晰的模块化单体:`foundation`、`domain` | Event Contract | 53 | 均已有 payload model,但当前全部是 diagnostic enforcement | | Python 源码量 | 约 271,400 行 | 60 个文件超过 1,000 行,14 个超过 2,000 行 | | 长方法 | 281 个超过 80 行 | 67 个超过 150 行,23 个超过 250 行;大量是私有方法 | -| 全量 mypy 历史债务 | 11,983 / 601 文件 | strict frontier 当前只覆盖 41 个文件,且 ratchet 已新增 2 个错误 | -| Ruff 历史诊断 | 929 | 低水位门禁通过,但规则集只覆盖 `E4/E7/E9/F/I` | -| 覆盖率低水位 | Application 78.24%,Domain 79.29% | Chain、Runtime、Agent、Adapter、Startup 未进入包级覆盖率门禁 | +| 全量 mypy 历史债务 | 11,827 / 596 文件 | strict frontier 当前覆盖 41 个文件,本批迁移路径的类型债务已清零 | +| Ruff 历史诊断 | 889 | 低水位门禁通过,但规则集只覆盖 `E4/E7/E9/F/I` | +| 覆盖率低水位 | Application 78.63%,Domain 79.29% | Chain、Runtime、Agent、Adapter、Startup 未进入包级覆盖率门禁 | ### 3.3 热点文件 diff --git a/docs/architecture-overview.md b/docs/architecture-overview.md index d63fe97a0..3a9265edd 100644 --- a/docs/architecture-overview.md +++ b/docs/architecture-overview.md @@ -200,7 +200,7 @@ Chain/Agent 的 `Any` factory 和裸 Oper 仍需迁移为类型化 Port/DTO, | `app/adapters/web/` | Web 技术适配:动态插件路由注册、认证依赖和 OpenAPI 重建;不承载插件路由用例 | `plugin/routes.py` | | `app/adapters/observability/` | 可选观测技术适配;核心层只依赖 `runtime/observability` 定义的窄端口 | `otel.py` | | `app/application/` | 读取配置/持久化状态的聚焦应用服务:识别、过滤、通知、RSS、站点、下载器、媒体服务器、存储、整理规则、可靠副作用等;同一主题拆成子包 | `recognition.py`、`rules.py`、`rss.py`、`outbox.py`、`site/`、`subscription/`、`plugin/` | -| `app/application/chain/` | Chain 运行时上下文、跨领域数据端口和 durable event 命令;将组合根注入的能力以命名 getter 暴露给 Chain | `context.py`、`data.py`、`durable_events.py` | +| `app/application/chain/` | Chain 运行时上下文、跨领域数据端口和 durable event 命令;将组合根注入的能力以命名 getter 暴露给 Chain | `context.py`、`data.py`、`events.py` | | `app/application/subscription/` | 订阅新增、查询、变更、删除、媒体身份与搜索契约 | `write.py`、`contract.py`、`mutation.py`、`delete.py`、`identity.py`、`search.py` | | `app/application/plugin/` | 插件市场、安装、运行时端口、文件夹操作和动态路由用例;具体 FastAPI 路由适配器在 adapters 层 | `catalog.py`、`install.py`、`runtime.py`、`folders.py`、`routes.py` | | `app/application/messaging/` | 渠道回环入口、消息渲染/路由、命令交互会话、插件按钮回调、Agent 消息桥接 | `ingress.py`、`message.py`、`router.py`、`agent.py` | @@ -458,7 +458,7 @@ flowchart LR |---|---| | **Config Reload** | 继承 `ConfigReloadMixin` 并声明 `CONFIG_WATCH`,配置变更时自动重建长生命周期对象(如下载器客户端重连) | | **Singleton** | `EventManager`、`ModuleManager`、`PluginManager` 等全局共享管理器继承 `foundation/singleton.py` 的 `Singleton` | -| **Managed Resource** | 可选进程级技术资源(浏览器、虚拟显示等)以 data-only `capability.toml` 声明,`runtime/extensions` 解释生命周期,`startup` 构建 Runtime,消费者经 `runtime/managed_resources.py` 显式获取;插件使用浏览器走 `app.sdk.browser` | +| **Managed Resource** | 可选进程级技术资源(浏览器、虚拟显示等)以 data-only `capability.toml` 声明,`runtime/extensions` 解释生命周期,`startup` 构建 Runtime,消费者经 `runtime/resources.py` 显式获取;插件使用浏览器走 `app.sdk.browser` | | **Observability** | `runtime/observability` 定义低基数指标和默认 no-op 端口,Startup 可选装配 OTel;HTTP、DB、Event、Module、Scheduler、插件生命周期和 Agent 只提交白名单标签 | --- @@ -631,7 +631,7 @@ flowchart LR RE["events.py
事件总线"] RL["log.py
日志运行时(依赖叶子)"] RCA["cache.py
缓存协议 / 内存后端 / 装饰器"] - MR["managed_resources.py
托管资源门面"] + MR["resources.py
托管资源门面"] end subgraph adapters["app/adapters(具体 I/O)"] @@ -704,8 +704,8 @@ flowchart LR | 指标 | 当前值 | |---|---:| -| Python 模块 | 842 | -| 内部导入边 | 6,882 | +| Python 模块 | 844 | +| 内部导入边 | 6,898 | | 非平凡 SCC | 2(`ARCH-107` 临时 Chain 包根环;精确 containment 的 TMDB 移植包环) | | Direct egress | 66(12 条待迁移债务,54 条精确 containment) | | Module Contract V2 spec | 217(其中 215 个进入 `run_module` 观察面) | diff --git a/docs/architecture-refactor-roadmap.md b/docs/architecture-refactor-roadmap.md index 03b312009..dbef163da 100644 --- a/docs/architecture-refactor-roadmap.md +++ b/docs/architecture-refactor-roadmap.md @@ -144,7 +144,7 @@ G-ARCH 只有在以下条件全部满足后才可完成: | S4-L2 Event strict contract | `PLANNED` | S0-L2.6,S1-L6 | 宿主事件输入/输出按风险 strict,诊断例外只属于第三方插件兼容 | | S4-L3 Complexity v2 | `PLANNED` | S3 | 私有方法、class/file、圈复杂度进入门禁;所有超限通过职责拆分归零 | | S4-L4 全量 mypy 清零 | `PLANNED` | S3,S4-L1,S4-L2 | `mypy-baseline.json` 归零并删除债务接受路径,全宿主 strict 类型通过 | -| S4-L5 Ruff 治理债务清零 | `PLANNED` | S3 | 当前受控 929 条诊断归零,规则集扩展经过独立审查且新增诊断为零 | +| S4-L5 Ruff 治理债务清零 | `PLANNED` | S3 | 当前受控 889 条诊断归零,规则集扩展经过独立审查且新增诊断为零 | | S4-L6 Coverage/并发/质量证据 | `PLANNED` | S3,S4-L1,S4-L2 | 高风险包纳入 coverage;raw concurrency 分类清零;Module Quality 有真实 evidence test | ### S5:Plugin、Agent、Domain、Startup 与最终收口 @@ -175,7 +175,7 @@ planning、lease、幂等执行或终态恢复已经完成。 **Ownership** -- `app/application/transfer.py` 拥有 admission DTO、Protocol、结果语义与 persist-before-enqueue 编排。 +- `app/application/transfer/workflow.py` 拥有 admission DTO、Protocol、结果语义与 persist-before-enqueue 编排。 - `app/db/adapters/` 提供短 Session/UoW 的 Transfer pending 持久化实现;`app/db/oper/` 只接收 adapter 拥有的 Session 并 stage/flush。 - `app/startup/` 负责构造并注入 adapter,宿主 Chain 不再取得 raw/`Any` `TransferPendingOper`。 @@ -242,11 +242,11 @@ git diff --check **Ownership and compatibility** -- `app/application/transfer.py` 拥有 planning input、plan item、checkpoint 和状态错误合同;JSON 版本、 +- `app/application/transfer/workflow.py` 拥有 planning input、plan item、checkpoint 和状态错误合同;JSON 版本、 指纹及 resolved 上下文均可跨进程 round-trip。 - `app/modules/filemanager/transhandler.py` 是唯一目标规划与文件执行实现;`FileManagerModule.transfer` 与 `TransHandler.transfer_media` 已删除,不保留第二套重命名、覆盖或目录递归逻辑。 -- `app/db/adapters/transfer.py` 通过短 Session/UoW 提交 checkpoint;Oper 只负责带状态和指纹条件的 +- `app/db/adapters/transfer/admission.py` 通过短 Session/UoW 提交 checkpoint;Oper 只负责带状态和指纹条件的 stage,3.0.14 migration 可升级、降级并在中断后重跑。 - cleanup intent 随准入输入冻结。宿主路径由 FileManager 在 `TransferIntercept` 放行后、任何文件写入前 执行;legacy provider 路径为保持旧 ABI 顺序,在全部冻结引用解析成功后、调用 provider 前执行。 diff --git a/docs/rules/05-architecture.md b/docs/rules/05-architecture.md index a30323c1c..c76f7e21d 100644 --- a/docs/rules/05-architecture.md +++ b/docs/rules/05-architecture.md @@ -65,10 +65,10 @@ to make the directory tree look symmetrical. | `app/application/search/` | Search state and later search-plan use cases | | `app/application/download/` | Download task querying/control and later submission use cases | | `app/application/music/` | Multi-source music catalog orchestration | -| `app/application/chain/` | Injectable Chain runtime context and compatibility provider | +| `app/application/chain/` | Injectable Chain runtime capabilities: `context.py` owns the runtime dependency aggregate, `data.py` owns named persistence ports, and `events.py` owns durable event write contracts plus replayable payload conversion | | `app/application/agentdata.py` | Named Agent data ports; canonical Agent consumers use `get_agent_*_port()` and do not alias legacy proxies to Oper classes | | `app/application/outbox.py` | Durable intent and Outbox repository/dispatcher contracts for post-commit side effects | -| `app/application/transfer_execution.py` | Durable transfer execution contracts: stable operation identity, step/checkpoint state, retry/manual-review commands and terminal-settlement DTOs; contains no SQLAlchemy or external I/O | +| `app/application/transfer/` | Durable transfer use cases: `workflow.py` owns admission/planning/queue behavior; `execution.py` owns stable operation identity, step/checkpoint state, retry/manual-review commands and terminal-settlement DTOs | | `app/application/plugin/` | Plugin market catalog, installation command, installed-plugin identity contract and startup migration, runtime port, folder operations and dynamic-route use cases; filenames remain single words (`catalog.py`, `identity.py`, `migration.py`, `install.py`, `runtime.py`, `folders.py`, `routes.py`) | | `app/application/server/` | MoviePilot Server reporting and sharing use cases; local data readers and transport callbacks are injected by startup | | `app/application/site/` | Configured site catalog, authentication level and index-resource capability; the generated extension and its data bundle stay together here | @@ -94,7 +94,7 @@ directory categories. | `app/runtime/observability/` | Low-cardinality metric contracts and no-op-capable observation facade | | `app/runtime/log.py` | Complete console/plugin/file logging runtime and shutdown | | `app/runtime/cache.py` | Cache protocols, memory implementations, decorators and proxies | -| `app/runtime/managed_resources.py` | Provider-neutral acquisition, observation and shutdown facade for process-owned optional resources | +| `app/runtime/resources.py` | Provider-neutral acquisition, observation and shutdown facade for process-owned optional resources | | `app/runtime/tasks.py` | Lifespan-scoped ownership, cancellation and bounded shutdown waiting for in-process background tasks | | `app/runtime/execution.py` | Shared sync/async execution and cross-thread submission boundary with correlation propagation | | `app/runtime/correlation.py` | Request/cross-thread correlation context and safe propagation into logs and child work | @@ -152,7 +152,7 @@ retained only for compatibility and is not a canonical Oper substitute. Durable transfer execution follows one explicit boundary. The Chain freezes each external file operation into the Application-owned contract in -`app/application/transfer_execution.py`; `app/db/adapters/transfer_execution.py` +`app/application/transfer/execution.py`; `app/db/adapters/transfer/execution.py` uses short transactions to persist the task ledger and fences every state change with the current lease and attempt token. `app/db/oper/transferexecutionstep.py` remains table-oriented and never owns retry or recovery policy. External file I/O @@ -206,7 +206,7 @@ mechanism remains in `app/adapters/system/resource.py`. 可选的进程级技术资源使用 Managed Resource 合同:实现及其 data-only `capability.toml` 与适配器同目录,`runtime/extensions` 只解释通用的同步/异步 `start`、`stop` 生命周期,`startup` 负责构建 Capability Runtime。声明必须使用 -`on_first_use`,普通启动只发现声明;消费者通过 `app/runtime/managed_resources.py` +`on_first_use`,普通启动只发现声明;消费者通过 `app/runtime/resources.py` 显式获取资源。关闭路径先释放消费者,再关闭已初始化 Runtime,未使用的资源不得因关闭而物化。 应用级启动顺序使用 `app/startup/lifecycle/components.py` 的组件描述声明依赖、 normal/safe-mode 范围、start/stop 顺序、超时预算和失败策略。新增进程级资源不得只在 @@ -484,9 +484,9 @@ Durable post-commit side effects have a separate boundary: must not replace an Outbox or persistent task table. Transfer durable admission follows the same ownership direction without using -the Outbox as an execution queue: `app/application/transfer.py` owns the typed +the Outbox as an execution queue: `app/application/transfer/workflow.py` owns the typed admission and versioned planning-checkpoint contracts, while -`app/db/adapters/transfer.py` commits admission and the +`app/db/adapters/transfer/admission.py` commits admission and the `accepted -> provider_pending -> planned` compare-and-set transitions in short Session/UoW scopes. `app/modules/filemanager/` owns the single pure-plan and checkpoint-execution implementation: all file writes occur @@ -504,8 +504,8 @@ command; `FileManagerModule.transfer` and `TransHandler.transfer_media` must not be recreated. Transfer execution ownership is orthogonal to those planning phases. -`app/application/transfer.py` defines the claim, heartbeat, release and fenced -mutation Port; `app/db/adapters/transfer.py` implements each operation in a +`app/application/transfer/workflow.py` defines the claim, heartbeat, release and fenced +mutation Port; `app/db/adapters/transfer/admission.py` implements each operation in a short UoW with a unique lease token. Any active lease rejects another claim, including one from the same process owner. Expired leases may be taken over with a new token and incremented attempt count, while the stale token cannot renew, @@ -659,8 +659,9 @@ driven workflow registration. | `app/application/subscription/write.py` | Subscription media translation and sync/async write-port orchestration | | `app/application/outbox.py` | Durable intent, topic handler and Outbox repository contracts | | `app/db/adapters/outbox.py` | SQLAlchemy Outbox persistence, claim/lease and retry state adapter | -| `app/application/transfer.py` | Transfer task, durable admission, versioned planning input/checkpoint contracts and queue use case | -| `app/db/adapters/transfer.py` | SQLAlchemy admission/checkpoint persistence, CAS state transition and detached snapshot adapter | +| `app/application/chain/events.py` | Chain durable-event write port, settlement projection and replayable payload conversion | +| `app/application/transfer/workflow.py` | Transfer task, durable admission, versioned planning input/checkpoint contracts and queue use case | +| `app/db/adapters/transfer/admission.py` | SQLAlchemy admission/checkpoint persistence, CAS state transition and detached snapshot adapter | | `app/application/scheduling.py` | Runtime scheduler facade for Agent tools and endpoints; `Scheduler` class registered by `app/startup/initializers/scheduler.py` | | `app/application/commands.py` | Command registry facade for Agent tools and endpoints; `Command` class registered by `app/startup/initializers/command.py` | | `app/application/workflow.py` | Workflow use cases plus the runtime port consumed by API and Chain; `WorkFlowManager` is registered by `app/startup/initializers/workflow.py` | @@ -700,8 +701,8 @@ driven workflow registration. | `app/adapters/external/plugin/client.py` | Plugin-market read adapter and cache-refresh boundary | | `app/adapters/system/plugin/package.py` | Plugin package installation adapter | | `app/adapters/system/plugin/dependency.py` | Plugin dependency inspection and installation adapter | -| `app/runtime/extensions/managed_resource_adapter.py` | Data-only managed-resource registry and sync/async lifecycle adapters | -| `app/runtime/managed_resources.py` | Lightweight acquisition, state observation and shutdown facade | +| `app/runtime/extensions/resource.py` | Data-only managed-resource registry and sync/async lifecycle adapters | +| `app/runtime/resources.py` | Lightweight acquisition, state observation and shutdown facade | | `app/foundation/reflection.py` | Generic reflection and Python module discovery | | `app/adapters/network/http.py` | Shared synchronous and asynchronous HTTP clients | | `app/adapters/network/browser.py` | Browser launch facade and browser session implementation | diff --git a/docs/rules/07-naming-conventions.md b/docs/rules/07-naming-conventions.md index 340f6b298..838e78b52 100644 --- a/docs/rules/07-naming-conventions.md +++ b/docs/rules/07-naming-conventions.md @@ -9,8 +9,9 @@ All new code must follow these conventions. Consistent naming is how the codebas | Context | Convention | Examples | |---|---|---| | Python source files | `snake_case.py` | `download.py`, `qbittorrent.py`, `package.py` | -| New files in canonical capability packages | Focused `snake_case.py`; prefer a package-owned noun and an existing owned domain file before adding one | `torrent.py`, `plugin_manager.py`, `package.py` | -| Module package directories | `snake_case/` | `qbittorrent/`, `synologychat/` | +| New files in canonical capability packages | Prefer one lowercase responsibility noun; extend an existing owner before adding a sibling file | `torrent.py`, `package.py`, `resources.py` | +| Multi-file capabilities | Create a same-named package and use focused single-word child files; do not flatten related `_.py` siblings | `transfer/workflow.py`, `transfer/execution.py` | +| Module package directories | `snake_case/`; package roots do not duplicate-export host implementations | `qbittorrent/`, `synologychat/`, `transfer/` | | Test files | `test_.py` | `test_download_chain.py`, `test_subscribe_endpoint.py` | | Alembic migrations | Auto-generated by Alembic; do not rename | `20240101_add_column.py` | | Skill directories | `/` | `transfer-failed-retry/`, `moviepilot-cli/` | @@ -118,6 +119,8 @@ All new code must follow these conventions. Consistent naming is how the codebas | `def handleConfigChanged():` | `def on_config_changed():` or `def handle_config_changed():` | | `configuration.get("RssUrls")` | `configuration.get(SystemConfigKey.RssUrls)` | | `class subscribe_oper:` | `class SubscribeOper:` | +| `transfer.py` + `transfer_execution.py` | `transfer/workflow.py` + `transfer/execution.py` | +| Package-root host re-exports for an old path | Exact SDK/Compat mapping; host code imports the owning child module | | `MessageChannel.Telegram`(新代码) | `NotificationChannel.Telegram` | | `Notification(title=...)`(新代码) | `Message(title=...)` | diff --git a/mypy.ini b/mypy.ini index 7ecbd4cf9..751b76ab9 100644 --- a/mypy.ini +++ b/mypy.ini @@ -28,7 +28,7 @@ files = app/application/scheduling.py, app/application/workflow.py, app/application/chain/context.py, - app/application/chain/durable_events.py, + app/application/chain/events.py, app/application/messaging/ingress.py, app/application/subscription/delete.py, app/application/subscription/identity.py, diff --git a/scripts/perf/instrument/sitecustomize.py b/scripts/perf/instrument/sitecustomize.py index 3fa40a18e..d6832db7e 100644 --- a/scripts/perf/instrument/sitecustomize.py +++ b/scripts/perf/instrument/sitecustomize.py @@ -367,7 +367,7 @@ def _activate_agent_scenario( def _read_display_runtime() -> dict[str, object]: """读取 host.display 的只读状态和观测,不触发资源激活。""" try: - from app.runtime.managed_resources import ( + from app.runtime.resources import ( managed_resource_observations, managed_resource_snapshot, ) diff --git a/scripts/perf/test_scenarios.py b/scripts/perf/test_scenarios.py index f111bcc40..d4c85568b 100644 --- a/scripts/perf/test_scenarios.py +++ b/scripts/perf/test_scenarios.py @@ -14,7 +14,6 @@ from types import ModuleType, SimpleNamespace import pytest - PERF_DIR = Path(__file__).resolve().parent @@ -651,7 +650,7 @@ def test_sitecustomize_serializes_managed_resource_facade(monkeypatch) -> None: error=None, ) - facade = ModuleType("app.runtime.managed_resources") + facade = ModuleType("app.runtime.resources") def managed_resource_snapshot(capability_id: str): assert capability_id == "host.display" @@ -663,7 +662,7 @@ def test_sitecustomize_serializes_managed_resource_facade(monkeypatch) -> None: facade.managed_resource_snapshot = managed_resource_snapshot facade.managed_resource_observations = managed_resource_observations - monkeypatch.setitem(sys.modules, "app.runtime.managed_resources", facade) + monkeypatch.setitem(sys.modules, "app.runtime.resources", facade) probe = load_module( "moviepilot_perf_sitecustomize_observation", PERF_DIR / "instrument" / "sitecustomize.py", diff --git a/tests/conftest.py b/tests/conftest.py index 770df6085..81e3ea798 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -23,7 +23,6 @@ prepare_backend() # 复用共享 autouse 网络守卫;同一实现亦供各插件仓 conftest import 复用,避免逐仓维护 from app.testing.network_guard import block_real_network # noqa: E402,F401 - TResult = TypeVar("TResult") @@ -104,31 +103,29 @@ def pytest_runtest_call(item): def configure_plugin_system_services(): """为绕过完整启动流程的单元测试装配真实插件系统适配器。""" from app.adapters.web.security.access import configure_token_codec - from app.application.security.token import ( - create_access_token, - decode_access_token, - ) from app.api.data import configure_api_data_ports from app.application.configuration import ( RuntimeConfiguration, RuntimeSettingsService, SystemConfigService, TransferRetryConfig, - configure_token_runtime_config, configure_runtime_configuration, configure_runtime_settings, configure_system_config, + configure_token_runtime_config, configure_transfer_retry_config, ) - from app.runtime.config import settings - from app.runtime.settings import configure_runtime_setting_provider - from app.startup.composition.configuration import ( - build_api_runtime_config, - build_chain_runtime_config, - build_scheduler_runtime_config, - build_token_runtime_config, + from app.application.security.token import ( + create_access_token, + decode_access_token, + ) + from app.application.security.userconfig import ( + UserConfigurationService, + configure_user_configuration, ) from app.application.service import configure_service_directory + from app.db.oper.systemconfig import SystemConfigOper + from app.db.oper.userconfig import UserConfigOper from app.db.session import ( SessionFactory, async_session_scope, @@ -140,11 +137,13 @@ def configure_plugin_system_services(): SqlAlchemyUnitOfWork, configure_transaction_runners, ) - from app.db.oper.systemconfig import SystemConfigOper - from app.db.oper.userconfig import UserConfigOper - from app.application.security.userconfig import ( - UserConfigurationService, - configure_user_configuration, + from app.runtime.config import settings + from app.runtime.settings import configure_runtime_setting_provider + from app.startup.composition.configuration import ( + build_api_runtime_config, + build_chain_runtime_config, + build_scheduler_runtime_config, + build_token_runtime_config, ) configure_token_codec(create_access_token, decode_access_token) @@ -181,25 +180,25 @@ def configure_plugin_system_services(): max_failed_retries=settings.TRANSFER_MAX_FAILED_RETRIES, ) ) - from app.application.chain.data import configure_chain_data_ports - from app.application.subscription.write import configure_subscribe_writer - from app.application.plugin.runtime import configure_plugin_runtime - from app.application.module import configure_module_runtime from app.application.chain.context import ( ChainRuntimeContext, configure_chain_runtime_context_provider, ) - from app.application.messaging.message import MessageHelper, MessageQueueManager + from app.application.chain.data import configure_chain_data_ports from app.application.messaging.chat import ( - AgentChatService, AgentChatPersistenceService, - configure_agent_chat_service, + AgentChatService, configure_agent_chat_persistence, + configure_agent_chat_service, ) + from app.application.messaging.message import MessageHelper, MessageQueueManager + from app.application.module import configure_module_runtime + from app.application.plugin.runtime import configure_plugin_runtime + from app.application.subscription.write import configure_subscribe_writer from app.runtime.cache import AsyncFileCache, FileCache from app.runtime.events import EventManager - from app.runtime.extensions.module_manager import ModuleManager from app.runtime.extensions.module.dispatcher import ModuleInvocationDispatcher + from app.runtime.extensions.module_manager import ModuleManager from app.runtime.extensions.plugin_manager import PluginManager from app.runtime.extensions.service_config import ServiceConfigHelper configure_service_directory( @@ -208,8 +207,8 @@ def configure_plugin_system_services(): ) configure_plugin_runtime(lambda: PluginManager()) configure_module_runtime(lambda: ModuleManager()) - from app.application.site.query import SiteQueryService, configure_site_query_service from app.application.site.health import SiteHealthService, configure_site_health_service + from app.application.site.query import SiteQueryService, configure_site_query_service from app.application.workflow import ( WorkflowQueryService, configure_workflow_query, @@ -222,27 +221,26 @@ def configure_plugin_system_services(): AgentTaskExecutionService, configure_agent_task_execution, ) + from app.db.adapters.download import TransactionalDownloadFailureRepository + from app.db.adapters.site import TransactionalSiteRepository + from app.db.adapters.subscription import TransactionalSubscribeWriter + from app.db.adapters.transaction import TransactionalWriteRunner + from app.db.adapters.transfer.admission import TransactionalTransferAdmissionRepository + from app.db.adapters.transfer.execution import ( + TransactionalTransferExecutionRepository, + ) + from app.db.adapters.workflow import TransactionalWorkflowExecutionService from app.db.oper.agentchat import AgentChatOper - from app.db.oper.downloadfailure import DownloadFailureOper from app.db.oper.downloadhistory import DownloadHistoryOper from app.db.oper.mediaserver import MediaServerOper + from app.db.oper.message import MessageOper + from app.db.oper.passkey import PassKeyOper from app.db.oper.site import SiteOper from app.db.oper.subscribe import SubscribeOper from app.db.oper.subscribehistory import SubscribeHistoryOper from app.db.oper.transferhistory import TransferHistoryOper - from app.db.adapters.transfer import TransactionalTransferAdmissionRepository - from app.db.adapters.transfer_execution import ( - TransactionalTransferExecutionRepository, - ) from app.db.oper.user import UserOper from app.db.oper.workflow import WorkflowOper, configure_workflow_legacy_writer - from app.db.oper.message import MessageOper - from app.db.oper.passkey import PassKeyOper - from app.db.adapters.subscription import TransactionalSubscribeWriter - from app.db.adapters.download import TransactionalDownloadFailureRepository - from app.db.adapters.site import TransactionalSiteRepository - from app.db.adapters.workflow import TransactionalWorkflowExecutionService - from app.db.adapters.transaction import TransactionalWriteRunner def create_sync_session() -> Session: """为无显式会话的 Oper 测试入口创建独占同步 Session。""" @@ -363,8 +361,8 @@ def configure_plugin_system_services(): ) configure_agent_chat_service(AgentChatService(repository=AgentChatOper())) from app.adapters.external.market import ( - PluginHelper, VERSION_BACKWARD_COMPATIBLE_FLAGS, + PluginHelper, ) from app.adapters.external.plugin.client import PluginMarketClient from app.adapters.system.plugin.dependency import PluginDependencyInstaller @@ -389,9 +387,9 @@ def configure_plugin_system_services(): frozen=lambda: False, install=lambda **_kwargs: (False, "测试环境未装配插件安装 Gateway"), )) - from app.agent.skills.registry import SkillHelper from app.agent.llm.gateway import register_llm_provider_runtime from app.agent.llm.provider import LLMProviderManager + from app.agent.skills.registry import SkillHelper from app.application.messaging.skill import register_skill_catalog_provider register_skill_catalog_provider(lambda: SkillHelper()) diff --git a/tests/fixtures/architecture/coverage-baseline.json b/tests/fixtures/architecture/coverage-baseline.json index a41d68151..43e601d8f 100644 --- a/tests/fixtures/architecture/coverage-baseline.json +++ b/tests/fixtures/architecture/coverage-baseline.json @@ -1,8 +1,8 @@ { "application": { - "covered_lines": 9966, - "percent": 78.61, - "statements": 12678 + "covered_lines": 9992, + "percent": 78.63, + "statements": 12707 }, "domain": { "covered_lines": 3392, diff --git a/tests/fixtures/architecture/dependency-baseline.json b/tests/fixtures/architecture/dependency-baseline.json index 50c467a19..15337674d 100644 --- a/tests/fixtures/architecture/dependency-baseline.json +++ b/tests/fixtures/architecture/dependency-baseline.json @@ -77,7 +77,7 @@ "target": "app.adapters.network.http" }, { - "source": "app.application.transfer", + "source": "app.application.transfer.workflow", "target": "app.adapters.system.host" }, { @@ -154,7 +154,7 @@ "app.application.security.cookie", "app.application.security.passkey", "app.application.torrent", - "app.application.transfer", + "app.application.transfer.workflow", "app.chain._recognition", "app.chain._transfer", "app.chain.download", @@ -1441,8 +1441,8 @@ "runtime_only": true } }, - "edge_count": 6882, - "edge_sha256": "602a73df30503fec4f4f01e28020222497cdadcad731dfedca40a3e4b1133c0d", + "edge_count": 6898, + "edge_sha256": "c73faf7e29ee12a862cf2332f1803fd4eb8a3f4d2994ed81075ff198e896f8df", "edges": [ "app -> app.runtime", "app -> app.runtime.compat", @@ -1532,7 +1532,7 @@ "app.adapters.network.browser -> app.adapters.network.http", "app.adapters.network.browser -> app.runtime", "app.adapters.network.browser -> app.runtime.log", - "app.adapters.network.browser -> app.runtime.managed_resources", + "app.adapters.network.browser -> app.runtime.resources", "app.adapters.network.browser -> app.runtime.settings", "app.adapters.network.cloudflare -> app.runtime", "app.adapters.network.cloudflare -> app.runtime.log", @@ -1553,7 +1553,7 @@ "app.adapters.system.display -> app.foundation.singleton", "app.adapters.system.display -> app.runtime", "app.adapters.system.display -> app.runtime.log", - "app.adapters.system.display -> app.runtime.managed_resources", + "app.adapters.system.display -> app.runtime.resources", "app.adapters.system.display.resource -> app.adapters", "app.adapters.system.display.resource -> app.adapters.system", "app.adapters.system.display.resource -> app.adapters.system.host", @@ -2219,7 +2219,8 @@ "app.agent.tools.impl.delete_transfer_history -> app.application.agentdata", "app.agent.tools.impl.delete_transfer_history -> app.application.chain", "app.agent.tools.impl.delete_transfer_history -> app.application.chain.data", - "app.agent.tools.impl.delete_transfer_history -> app.application.transfer_execution", + "app.agent.tools.impl.delete_transfer_history -> app.application.transfer", + "app.agent.tools.impl.delete_transfer_history -> app.application.transfer.execution", "app.agent.tools.impl.delete_transfer_history -> app.chain", "app.agent.tools.impl.delete_transfer_history -> app.chain.storage", "app.agent.tools.impl.delete_transfer_history -> app.runtime", @@ -3313,7 +3314,8 @@ "app.api.endpoints.history -> app.application.chain.data", "app.api.endpoints.history -> app.application.configuration", "app.api.endpoints.history -> app.application.history", - "app.api.endpoints.history -> app.application.transfer_execution", + "app.api.endpoints.history -> app.application.transfer", + "app.api.endpoints.history -> app.application.transfer.execution", "app.api.endpoints.history -> app.runtime", "app.api.endpoints.history -> app.runtime.config", "app.api.endpoints.history -> app.runtime.log", @@ -3824,7 +3826,8 @@ "app.api.endpoints.transfer -> app.application.configuration", "app.api.endpoints.transfer -> app.application.directory", "app.api.endpoints.transfer -> app.application.history", - "app.api.endpoints.transfer -> app.application.transfer_execution", + "app.api.endpoints.transfer -> app.application.transfer", + "app.api.endpoints.transfer -> app.application.transfer.execution", "app.api.endpoints.transfer -> app.chain", "app.api.endpoints.transfer -> app.chain.media", "app.api.endpoints.transfer -> app.chain.transfer", @@ -3977,26 +3980,28 @@ "app.application.chain.context -> app.application", "app.application.chain.context -> app.application.chain", "app.application.chain.context -> app.application.chain.data", - "app.application.chain.context -> app.application.chain.durable_events", + "app.application.chain.context -> app.application.chain.events", "app.application.chain.context -> app.application.configuration", "app.application.chain.context -> app.runtime", "app.application.chain.context -> app.runtime.stop", "app.application.chain.data -> app.application", "app.application.chain.data -> app.application.transfer", - "app.application.chain.data -> app.application.transfer_execution", - "app.application.chain.durable_events -> app.application", - "app.application.chain.durable_events -> app.application.history", - "app.application.chain.durable_events -> app.application.transfer_execution", - "app.application.chain.durable_events -> app.domain", - "app.application.chain.durable_events -> app.domain.context", - "app.application.chain.durable_events -> app.domain.meta", - "app.application.chain.durable_events -> app.domain.meta.metabase", - "app.application.chain.durable_events -> app.domain.meta.metamusic", - "app.application.chain.durable_events -> app.domain.metainfo", - "app.application.chain.durable_events -> app.schemas", - "app.application.chain.durable_events -> app.schemas.file", - "app.application.chain.durable_events -> app.schemas.transfer", - "app.application.chain.durable_events -> app.schemas.types", + "app.application.chain.data -> app.application.transfer.execution", + "app.application.chain.data -> app.application.transfer.workflow", + "app.application.chain.events -> app.application", + "app.application.chain.events -> app.application.history", + "app.application.chain.events -> app.application.transfer", + "app.application.chain.events -> app.application.transfer.execution", + "app.application.chain.events -> app.domain", + "app.application.chain.events -> app.domain.context", + "app.application.chain.events -> app.domain.meta", + "app.application.chain.events -> app.domain.meta.metabase", + "app.application.chain.events -> app.domain.meta.metamusic", + "app.application.chain.events -> app.domain.metainfo", + "app.application.chain.events -> app.schemas", + "app.application.chain.events -> app.schemas.file", + "app.application.chain.events -> app.schemas.transfer", + "app.application.chain.events -> app.schemas.types", "app.application.configuration -> app.application", "app.application.configuration -> app.application.database", "app.application.configuration -> app.schemas", @@ -4441,31 +4446,33 @@ "app.application.torrent_cache -> app.foundation.crypto", "app.application.torrent_cache -> app.schemas", "app.application.torrent_cache -> app.schemas.types", - "app.application.transfer -> app.adapters", - "app.application.transfer -> app.adapters.system", - "app.application.transfer -> app.adapters.system.host", - "app.application.transfer -> app.application", - "app.application.transfer -> app.application.agent", - "app.application.transfer -> app.application.transfer_execution", - "app.application.transfer -> app.domain", - "app.application.transfer -> app.domain.context", - "app.application.transfer -> app.domain.media", - "app.application.transfer -> app.domain.meta", - "app.application.transfer -> app.domain.meta.metabase", - "app.application.transfer -> app.domain.meta.metamusic", - "app.application.transfer -> app.foundation", - "app.application.transfer -> app.foundation.text", - "app.application.transfer -> app.runtime", - "app.application.transfer -> app.runtime.log", - "app.application.transfer -> app.schemas", - "app.application.transfer -> app.schemas.file", - "app.application.transfer -> app.schemas.history", - "app.application.transfer -> app.schemas.media", - "app.application.transfer -> app.schemas.system", - "app.application.transfer -> app.schemas.tmdb", - "app.application.transfer -> app.schemas.transfer", - "app.application.transfer -> app.schemas.types", - "app.application.transfer -> app.schemas.workflow", + "app.application.transfer.workflow -> app.adapters", + "app.application.transfer.workflow -> app.adapters.system", + "app.application.transfer.workflow -> app.adapters.system.host", + "app.application.transfer.workflow -> app.application", + "app.application.transfer.workflow -> app.application.agent", + "app.application.transfer.workflow -> app.application.transfer", + "app.application.transfer.workflow -> app.application.transfer.execution", + "app.application.transfer.workflow -> app.domain", + "app.application.transfer.workflow -> app.domain.context", + "app.application.transfer.workflow -> app.domain.media", + "app.application.transfer.workflow -> app.domain.meta", + "app.application.transfer.workflow -> app.domain.meta.metabase", + "app.application.transfer.workflow -> app.domain.meta.metamusic", + "app.application.transfer.workflow -> app.foundation", + "app.application.transfer.workflow -> app.foundation.text", + "app.application.transfer.workflow -> app.runtime", + "app.application.transfer.workflow -> app.runtime.log", + "app.application.transfer.workflow -> app.schemas", + "app.application.transfer.workflow -> app.schemas.context", + "app.application.transfer.workflow -> app.schemas.file", + "app.application.transfer.workflow -> app.schemas.history", + "app.application.transfer.workflow -> app.schemas.media", + "app.application.transfer.workflow -> app.schemas.music", + "app.application.transfer.workflow -> app.schemas.system", + "app.application.transfer.workflow -> app.schemas.tmdb", + "app.application.transfer.workflow -> app.schemas.transfer", + "app.application.transfer.workflow -> app.schemas.types", "app.chain -> app.application", "app.chain -> app.application.chain", "app.chain -> app.application.chain.context", @@ -4568,7 +4575,8 @@ "app.chain._transfer -> app.application.formatting", "app.chain._transfer -> app.application.history", "app.chain._transfer -> app.application.transfer", - "app.chain._transfer -> app.application.transfer_execution", + "app.chain._transfer -> app.application.transfer.execution", + "app.chain._transfer -> app.application.transfer.workflow", "app.chain._transfer -> app.chain", "app.chain._transfer -> app.chain._contracts", "app.chain._transfer -> app.chain.media", @@ -4994,14 +5002,15 @@ "app.chain.transfer -> app.application", "app.chain.transfer -> app.application.chain", "app.chain.transfer -> app.application.chain.data", - "app.chain.transfer -> app.application.chain.durable_events", + "app.chain.transfer -> app.application.chain.events", "app.chain.transfer -> app.application.configuration", "app.chain.transfer -> app.application.directory", "app.chain.transfer -> app.application.formatting", "app.chain.transfer -> app.application.history", "app.chain.transfer -> app.application.outbox", "app.chain.transfer -> app.application.transfer", - "app.chain.transfer -> app.application.transfer_execution", + "app.chain.transfer -> app.application.transfer.execution", + "app.chain.transfer -> app.application.transfer.workflow", "app.chain.transfer -> app.chain", "app.chain.transfer -> app.chain._transfer", "app.chain.transfer -> app.chain.media", @@ -5105,10 +5114,11 @@ "app.command -> app.schemas.types", "app.db.adapters.chain -> app.application", "app.db.adapters.chain -> app.application.chain", - "app.db.adapters.chain -> app.application.chain.durable_events", + "app.db.adapters.chain -> app.application.chain.events", "app.db.adapters.chain -> app.application.history", "app.db.adapters.chain -> app.application.outbox", - "app.db.adapters.chain -> app.application.transfer_execution", + "app.db.adapters.chain -> app.application.transfer", + "app.db.adapters.chain -> app.application.transfer.execution", "app.db.adapters.chain -> app.db", "app.db.adapters.chain -> app.db.adapters", "app.db.adapters.chain -> app.db.adapters.outbox", @@ -5165,24 +5175,26 @@ "app.db.adapters.subscription -> app.db.uow", "app.db.adapters.transaction -> app.db", "app.db.adapters.transaction -> app.db.uow", - "app.db.adapters.transfer -> app.application", - "app.db.adapters.transfer -> app.application.transfer", - "app.db.adapters.transfer -> app.db", - "app.db.adapters.transfer -> app.db.models", - "app.db.adapters.transfer -> app.db.models.transferpending", - "app.db.adapters.transfer -> app.db.oper", - "app.db.adapters.transfer -> app.db.oper.transferpending", - "app.db.adapters.transfer -> app.db.uow", - "app.db.adapters.transfer_execution -> app.application", - "app.db.adapters.transfer_execution -> app.application.transfer_execution", - "app.db.adapters.transfer_execution -> app.db", - "app.db.adapters.transfer_execution -> app.db.models", - "app.db.adapters.transfer_execution -> app.db.models.transferexecutionstep", - "app.db.adapters.transfer_execution -> app.db.models.transferpending", - "app.db.adapters.transfer_execution -> app.db.oper", - "app.db.adapters.transfer_execution -> app.db.oper.transferexecutionstep", - "app.db.adapters.transfer_execution -> app.db.oper.transferpending", - "app.db.adapters.transfer_execution -> app.db.uow", + "app.db.adapters.transfer.admission -> app.application", + "app.db.adapters.transfer.admission -> app.application.transfer", + "app.db.adapters.transfer.admission -> app.application.transfer.workflow", + "app.db.adapters.transfer.admission -> app.db", + "app.db.adapters.transfer.admission -> app.db.models", + "app.db.adapters.transfer.admission -> app.db.models.transferpending", + "app.db.adapters.transfer.admission -> app.db.oper", + "app.db.adapters.transfer.admission -> app.db.oper.transferpending", + "app.db.adapters.transfer.admission -> app.db.uow", + "app.db.adapters.transfer.execution -> app.application", + "app.db.adapters.transfer.execution -> app.application.transfer", + "app.db.adapters.transfer.execution -> app.application.transfer.execution", + "app.db.adapters.transfer.execution -> app.db", + "app.db.adapters.transfer.execution -> app.db.models", + "app.db.adapters.transfer.execution -> app.db.models.transferexecutionstep", + "app.db.adapters.transfer.execution -> app.db.models.transferpending", + "app.db.adapters.transfer.execution -> app.db.oper", + "app.db.adapters.transfer.execution -> app.db.oper.transferexecutionstep", + "app.db.adapters.transfer.execution -> app.db.oper.transferpending", + "app.db.adapters.transfer.execution -> app.db.uow", "app.db.adapters.workflow -> app.application", "app.db.adapters.workflow -> app.application.workflow", "app.db.adapters.workflow -> app.db", @@ -5852,7 +5864,8 @@ "app.modules.filemanager.module -> app.application.messaging", "app.modules.filemanager.module -> app.application.messaging.message", "app.modules.filemanager.module -> app.application.transfer", - "app.modules.filemanager.module -> app.application.transfer_execution", + "app.modules.filemanager.module -> app.application.transfer.execution", + "app.modules.filemanager.module -> app.application.transfer.workflow", "app.modules.filemanager.module -> app.domain", "app.modules.filemanager.module -> app.domain.context", "app.modules.filemanager.module -> app.domain.meta", @@ -6005,7 +6018,8 @@ "app.modules.filemanager.transhandler -> app.application.messaging", "app.modules.filemanager.transhandler -> app.application.messaging.message", "app.modules.filemanager.transhandler -> app.application.transfer", - "app.modules.filemanager.transhandler -> app.application.transfer_execution", + "app.modules.filemanager.transhandler -> app.application.transfer.execution", + "app.modules.filemanager.transhandler -> app.application.transfer.workflow", "app.modules.filemanager.transhandler -> app.domain", "app.modules.filemanager.transhandler -> app.domain.context", "app.modules.filemanager.transhandler -> app.domain.meta", @@ -7351,12 +7365,6 @@ "app.runtime.extensions.host_module_adapter -> app.runtime.settings", "app.runtime.extensions.host_module_adapter -> app.schemas", "app.runtime.extensions.host_module_adapter -> app.schemas.types", - "app.runtime.extensions.managed_resource_adapter -> app.runtime", - "app.runtime.extensions.managed_resource_adapter -> app.runtime.capabilities", - "app.runtime.extensions.managed_resource_adapter -> app.runtime.capabilities.errors", - "app.runtime.extensions.managed_resource_adapter -> app.runtime.capabilities.model", - "app.runtime.extensions.managed_resource_adapter -> app.runtime.capabilities.registry", - "app.runtime.extensions.managed_resource_adapter -> app.runtime.managed_resources", "app.runtime.extensions.module.dispatcher -> app.foundation", "app.runtime.extensions.module.dispatcher -> app.foundation.reflection", "app.runtime.extensions.module.dispatcher -> app.runtime", @@ -7479,6 +7487,12 @@ "app.runtime.extensions.plugin_manager -> app.schemas.exception", "app.runtime.extensions.plugin_manager -> app.schemas.plugin", "app.runtime.extensions.plugin_manager -> app.schemas.types", + "app.runtime.extensions.resource -> app.runtime", + "app.runtime.extensions.resource -> app.runtime.capabilities", + "app.runtime.extensions.resource -> app.runtime.capabilities.errors", + "app.runtime.extensions.resource -> app.runtime.capabilities.model", + "app.runtime.extensions.resource -> app.runtime.capabilities.registry", + "app.runtime.extensions.resource -> app.runtime.resources", "app.runtime.extensions.service_config -> app.runtime", "app.runtime.extensions.service_config -> app.runtime.log", "app.runtime.extensions.service_config -> app.schemas", @@ -7670,6 +7684,7 @@ "app.sdk._legacy.subscribe -> app.domain.context", "app.sdk._legacy.transfer -> app.application", "app.sdk._legacy.transfer -> app.application.transfer", + "app.sdk._legacy.transfer -> app.application.transfer.workflow", "app.sdk._legacy.transferpending -> app.db", "app.sdk._legacy.transferpending -> app.db.base", "app.sdk._legacy.transferpending -> app.db.models", @@ -7890,12 +7905,6 @@ "app.startup.initializers.domain -> app.domain.metainfo", "app.startup.initializers.domain -> app.runtime", "app.startup.initializers.domain -> app.runtime.settings", - "app.startup.initializers.managed_resources -> app.runtime", - "app.startup.initializers.managed_resources -> app.runtime.capabilities", - "app.startup.initializers.managed_resources -> app.runtime.capabilities.runtime", - "app.startup.initializers.managed_resources -> app.runtime.extensions", - "app.startup.initializers.managed_resources -> app.runtime.extensions.managed_resource_adapter", - "app.startup.initializers.managed_resources -> app.runtime.managed_resources", "app.startup.initializers.modules -> app.adapters", "app.startup.initializers.modules -> app.adapters.cache", "app.startup.initializers.modules -> app.adapters.cache.redis", @@ -7918,7 +7927,7 @@ "app.startup.initializers.modules -> app.application.chain", "app.startup.initializers.modules -> app.application.chain.context", "app.startup.initializers.modules -> app.application.chain.data", - "app.startup.initializers.modules -> app.application.chain.durable_events", + "app.startup.initializers.modules -> app.application.chain.events", "app.startup.initializers.modules -> app.application.configuration", "app.startup.initializers.modules -> app.application.database", "app.startup.initializers.modules -> app.application.history", @@ -7970,7 +7979,8 @@ "app.startup.initializers.modules -> app.db.adapters.subscription", "app.startup.initializers.modules -> app.db.adapters.transaction", "app.startup.initializers.modules -> app.db.adapters.transfer", - "app.startup.initializers.modules -> app.db.adapters.transfer_execution", + "app.startup.initializers.modules -> app.db.adapters.transfer.admission", + "app.startup.initializers.modules -> app.db.adapters.transfer.execution", "app.startup.initializers.modules -> app.db.adapters.workflow", "app.startup.initializers.modules -> app.db.oper", "app.startup.initializers.modules -> app.db.oper.agentchat", @@ -8021,7 +8031,7 @@ "app.startup.initializers.modules -> app.startup.composition.subscription", "app.startup.initializers.modules -> app.startup.initializers", "app.startup.initializers.modules -> app.startup.initializers.agent", - "app.startup.initializers.modules -> app.startup.initializers.managed_resources", + "app.startup.initializers.modules -> app.startup.initializers.resources", "app.startup.initializers.monitor -> app.monitor", "app.startup.initializers.monitor -> app.runtime", "app.startup.initializers.monitor -> app.runtime.execution", @@ -8075,12 +8085,18 @@ "app.startup.initializers.plugins -> app.runtime.extensions.plugin.system", "app.startup.initializers.plugins -> app.runtime.extensions.plugin_manager", "app.startup.initializers.plugins -> app.runtime.log", - "app.startup.initializers.plugins -> app.runtime.managed_resources", + "app.startup.initializers.plugins -> app.runtime.resources", "app.startup.initializers.plugins -> app.runtime.settings", "app.startup.initializers.plugins -> app.schemas", "app.startup.initializers.plugins -> app.schemas.exception", "app.startup.initializers.plugins -> app.schemas.plugin", "app.startup.initializers.plugins -> app.schemas.types", + "app.startup.initializers.resources -> app.runtime", + "app.startup.initializers.resources -> app.runtime.capabilities", + "app.startup.initializers.resources -> app.runtime.capabilities.runtime", + "app.startup.initializers.resources -> app.runtime.extensions", + "app.startup.initializers.resources -> app.runtime.extensions.resource", + "app.startup.initializers.resources -> app.runtime.resources", "app.startup.initializers.routers -> app.api", "app.startup.initializers.routers -> app.api.router_specs", "app.startup.initializers.routers -> app.api.servarr", @@ -8327,7 +8343,7 @@ "app.workflow.actions.transfer_file -> app.workflow", "app.workflow.actions.transfer_file -> app.workflow.actions" ], - "module_count": 842, + "module_count": 844, "modules": [ "app", "app.adapters", @@ -8583,7 +8599,7 @@ "app.application.chain", "app.application.chain.context", "app.application.chain.data", - "app.application.chain.durable_events", + "app.application.chain.events", "app.application.commands", "app.application.configuration", "app.application.dashboard", @@ -8674,7 +8690,8 @@ "app.application.torrent", "app.application.torrent_cache", "app.application.transfer", - "app.application.transfer_execution", + "app.application.transfer.execution", + "app.application.transfer.workflow", "app.application.workflow", "app.chain", "app.chain._contracts", @@ -8726,7 +8743,8 @@ "app.db.adapters.subscription", "app.db.adapters.transaction", "app.db.adapters.transfer", - "app.db.adapters.transfer_execution", + "app.db.adapters.transfer.admission", + "app.db.adapters.transfer.execution", "app.db.adapters.workflow", "app.db.base", "app.db.decorators", @@ -9025,7 +9043,6 @@ "app.runtime.execution", "app.runtime.extensions", "app.runtime.extensions.host_module_adapter", - "app.runtime.extensions.managed_resource_adapter", "app.runtime.extensions.module", "app.runtime.extensions.module.contracts", "app.runtime.extensions.module.dispatcher", @@ -9050,16 +9067,17 @@ "app.runtime.extensions.plugin.system", "app.runtime.extensions.plugin.tools", "app.runtime.extensions.plugin_manager", + "app.runtime.extensions.resource", "app.runtime.extensions.service_config", "app.runtime.gc", "app.runtime.health", "app.runtime.localization", "app.runtime.log", - "app.runtime.managed_resources", "app.runtime.observability", "app.runtime.progress", "app.runtime.rate", "app.runtime.reload", + "app.runtime.resources", "app.runtime.scheduling", "app.runtime.settings", "app.runtime.state", @@ -9140,10 +9158,10 @@ "app.startup.initializers.command", "app.startup.initializers.database", "app.startup.initializers.domain", - "app.startup.initializers.managed_resources", "app.startup.initializers.modules", "app.startup.initializers.monitor", "app.startup.initializers.plugins", + "app.startup.initializers.resources", "app.startup.initializers.routers", "app.startup.initializers.scheduler", "app.startup.initializers.transfer", diff --git a/tests/fixtures/architecture/dependency-policy.json b/tests/fixtures/architecture/dependency-policy.json index 69452db64..82a27296f 100644 --- a/tests/fixtures/architecture/dependency-policy.json +++ b/tests/fixtures/architecture/dependency-policy.json @@ -142,7 +142,7 @@ "tracking": "S2-L6" }, { - "source": "app.application.transfer", + "source": "app.application.transfer.workflow", "target": "app.adapters.system.host", "tracking": "S2-L6" }, diff --git a/tests/fixtures/architecture/mypy-baseline.json b/tests/fixtures/architecture/mypy-baseline.json index 81ad20cc1..7c2a61cdc 100644 --- a/tests/fixtures/architecture/mypy-baseline.json +++ b/tests/fixtures/architecture/mypy-baseline.json @@ -1112,10 +1112,6 @@ "app/application/agenttask.py": { "type-arg": 1 }, - "app/application/chain/durable_events.py": { - "arg-type": 1, - "no-untyped-call": 1 - }, "app/application/commands.py": { "no-any-return": 1 }, @@ -1356,18 +1352,6 @@ "app/application/torrent_cache.py": { "type-arg": 1 }, - "app/application/transfer.py": { - "arg-type": 16, - "assignment": 3, - "attr-defined": 4, - "misc": 6, - "no-any-return": 1, - "no-untyped-call": 2, - "no-untyped-def": 10, - "return-value": 1, - "type-arg": 14, - "union-attr": 47 - }, "app/chain/__init__.py": { "assignment": 13, "attr-defined": 1, @@ -3210,10 +3194,6 @@ "app/runtime/localization.py": { "no-any-return": 1 }, - "app/runtime/managed_resources.py": { - "arg-type": 2, - "union-attr": 3 - }, "app/runtime/progress.py": { "no-any-return": 4, "type-arg": 6 diff --git a/tests/fixtures/architecture/ruff-baseline.json b/tests/fixtures/architecture/ruff-baseline.json index e0a61008d..8a6fa8018 100644 --- a/tests/fixtures/architecture/ruff-baseline.json +++ b/tests/fixtures/architecture/ruff-baseline.json @@ -15,9 +15,6 @@ "F841": 2, "I001": 1 }, - "app/adapters/system/display/__init__.py": { - "I001": 1 - }, "app/adapters/system/host.py": { "I001": 2 }, @@ -300,9 +297,6 @@ "app/application/agenttask.py": { "I001": 1 }, - "app/application/chain/data.py": { - "I001": 1 - }, "app/application/directory.py": { "I001": 1 }, @@ -392,10 +386,6 @@ "app/chain/_music.py": { "E402": 5 }, - "app/chain/_transfer.py": { - "E402": 15, - "F401": 1 - }, "app/chain/media.py": { "E731": 2 }, @@ -962,9 +952,6 @@ "app/runtime/extensions/host_module_adapter.py": { "I001": 1 }, - "app/runtime/extensions/managed_resource_adapter.py": { - "I001": 1 - }, "app/runtime/extensions/module_manager.py": { "I001": 1 }, @@ -990,9 +977,6 @@ "E731": 1, "I001": 1 }, - "app/runtime/managed_resources.py": { - "I001": 1 - }, "app/runtime/observability/__init__.py": { "I001": 1 }, @@ -1038,18 +1022,12 @@ "app/schemas/system.py": { "I001": 1 }, - "app/schemas/transfer.py": { - "I001": 1 - }, "app/schemas/types.py": { "I001": 1 }, "app/schemas/user.py": { "I001": 1 }, - "app/sdk/_legacy/transfer.py": { - "I001": 1 - }, "app/sdk/_legacy/user.py": { "I001": 1 }, @@ -1125,9 +1103,6 @@ "scripts/perf/task_registry_ab.py": { "E402": 1 }, - "scripts/perf/test_scenarios.py": { - "I001": 1 - }, "scripts/schema/exports.py": { "I001": 1 }, @@ -1137,10 +1112,6 @@ "scripts/startup/performance.py": { "I001": 1 }, - "tests/conftest.py": { - "F401": 1, - "I001": 7 - }, "tests/test_acoustid_module.py": { "I001": 1 }, @@ -1440,12 +1411,6 @@ "tests/test_main_direct_execution.py": { "I001": 1 }, - "tests/test_managed_resources.py": { - "I001": 1 - }, - "tests/test_manual_transfer_history.py": { - "I001": 1 - }, "tests/test_mcp_plugin_tools.py": { "I001": 1 }, @@ -1797,9 +1762,6 @@ "E402": 6, "I001": 2 }, - "tests/test_transfer_failure_notification_aggregation.py": { - "I001": 1 - }, "tests/test_transfer_history_gate.py": { "I001": 1 }, @@ -1809,27 +1771,12 @@ "tests/test_transfer_history_write_path.py": { "I001": 1 }, - "tests/test_transfer_mark_torrent_completed.py": { - "I001": 1 - }, - "tests/test_transfer_mounted_disk_cleanup.py": { - "I001": 1 - }, - "tests/test_transfer_movie_collection.py": { - "I001": 1 - }, "tests/test_transfer_queue_count.py": { "I001": 1 }, "tests/test_transfer_rename_build_event.py": { "I001": 1 }, - "tests/test_transfer_stale_tasks.py": { - "I001": 1 - }, - "tests/test_transfer_tmdb_category.py": { - "I001": 1 - }, "tests/test_transferhistory_media_source_migration.py": { "I001": 1 }, diff --git a/tests/fixtures/architecture/runtime-contract-baseline.json b/tests/fixtures/architecture/runtime-contract-baseline.json index 5a69ca4a6..14942cc72 100644 --- a/tests/fixtures/architecture/runtime-contract-baseline.json +++ b/tests/fixtures/architecture/runtime-contract-baseline.json @@ -1,6 +1,13 @@ { "compat_manifest": { "module_aliases": { + "app.application.chain.durable_events": { + "introduced": "v3.0.0", + "is_package": false, + "owner": "application", + "replacement": "app.application.chain.events", + "target": "app.application.chain.events" + }, "app.application.filter": { "introduced": "v3.0.0", "is_package": false, @@ -15,6 +22,13 @@ "replacement": "app.application.rules", "target": "app.application.rules" }, + "app.application.transfer_execution": { + "introduced": "v3.0.0", + "is_package": false, + "owner": "application", + "replacement": "app.application.transfer.execution", + "target": "app.application.transfer.execution" + }, "app.chain.media_interaction": { "introduced": "v3.0.0", "is_package": false, @@ -250,7 +264,7 @@ "introduced": "v3.0.0", "is_package": false, "owner": "sdk", - "replacement": "app.application.transfer", + "replacement": "app.application.transfer.workflow", "target": "app.sdk._legacy.transferpending" }, "app.db.user_oper": { @@ -596,6 +610,13 @@ "replacement": "app.sdk.services", "target": "app.sdk.services" }, + "app.runtime.managed_resources": { + "introduced": "v3.0.0", + "is_package": false, + "owner": "runtime", + "replacement": "app.runtime.resources", + "target": "app.runtime.resources" + }, "app.utils.coalesce": { "introduced": "v3.0.0", "is_package": false, @@ -869,6 +890,18 @@ "target_name": "ReplyMode" } }, + "app.application.transfer": { + "TransferQueue": { + "replacement": "app.application.transfer.workflow.TransferQueue", + "target_module": "app.sdk._legacy.transfer", + "target_name": "TransferQueue" + }, + "TransferTask": { + "replacement": "app.application.transfer.workflow.TransferTask", + "target_module": "app.sdk._legacy.transfer", + "target_name": "TransferTask" + } + }, "app.chain.media": { "ScrapingChain": { "replacement": "app.chain.scraping.ScrapingChain", @@ -987,12 +1020,12 @@ "target_name": "MessageType" }, "TransferQueue": { - "replacement": "app.application.transfer.TransferQueue", + "replacement": "app.application.transfer.workflow.TransferQueue", "target_module": "app.sdk._legacy.transfer", "target_name": "TransferQueue" }, "TransferTask": { - "replacement": "app.application.transfer.TransferTask", + "replacement": "app.application.transfer.workflow.TransferTask", "target_module": "app.sdk._legacy.transfer", "target_name": "TransferTask" } @@ -1083,12 +1116,12 @@ "target_name": "TransferDirectoryConf" }, "TransferQueue": { - "replacement": "app.application.transfer.TransferQueue", + "replacement": "app.application.transfer.workflow.TransferQueue", "target_module": "app.sdk._legacy.transfer", "target_name": "TransferQueue" }, "TransferTask": { - "replacement": "app.application.transfer.TransferTask", + "replacement": "app.application.transfer.workflow.TransferTask", "target_module": "app.sdk._legacy.transfer", "target_name": "TransferTask" } diff --git a/tests/fixtures/architecture/startup-performance-baseline.json b/tests/fixtures/architecture/startup-performance-baseline.json index 9ef42c5c6..7d8e7c78b 100644 --- a/tests/fixtures/architecture/startup-performance-baseline.json +++ b/tests/fixtures/architecture/startup-performance-baseline.json @@ -1,41 +1,41 @@ { "schema_version": 2, - "generated_at": "2026-08-27T12:39:02.223653+00:00", + "generated_at": "2026-08-27T13:33:52.081319+00:00", "platform": "macOS-26.5.2-arm64-arm-64bit-Mach-O", "python": "3.14.3", "repeat": 3, "targets": { "app.startup.lifecycle": { - "loaded_app_module_count": 386, - "max_ms": 943.849, - "median_ms": 933.725, - "min_ms": 933.213, + "loaded_app_module_count": 388, + "max_ms": 1035.341, + "median_ms": 993.245, + "min_ms": 991.998, "samples_ms": [ - 943.849, - 933.213, - 933.725 + 1035.341, + 991.998, + 993.245 ] }, "app.factory": { - "loaded_app_module_count": 398, - "max_ms": 959.917, - "median_ms": 955.782, - "min_ms": 950.477, + "loaded_app_module_count": 400, + "max_ms": 1006.172, + "median_ms": 1005.88, + "min_ms": 1003.841, "samples_ms": [ - 950.477, - 955.782, - 959.917 + 1005.88, + 1006.172, + 1003.841 ] }, "app.main": { - "loaded_app_module_count": 400, - "max_ms": 1027.215, - "median_ms": 1012.512, - "min_ms": 996.609, + "loaded_app_module_count": 402, + "max_ms": 1085.652, + "median_ms": 1055.994, + "min_ms": 1043.212, "samples_ms": [ - 1027.215, - 1012.512, - 996.609 + 1055.994, + 1043.212, + 1085.652 ] } }, @@ -47,24 +47,24 @@ { "mode": "normal", "enabled_component_count": 25, - "startup_ms": 0.668, - "full_lifespan_ms": 1.475, + "startup_ms": 0.702, + "full_lifespan_ms": 1.54, "stage_ms": { - "后台任务登记器": 0.081, - "数据库准备": 0.039, - "HTTP 基础能力": 0.032, - "领域依赖装配": 0.029, - "数据库引擎预热": 0.026, - "数据库连接预算": 0.025, - "路由": 0.023, - "模块服务": 0.026, - "插件备份恢复": 0.023, + "后台任务登记器": 0.09, + "数据库准备": 0.048, + "HTTP 基础能力": 0.03, + "领域依赖装配": 0.033, + "数据库引擎预热": 0.031, + "数据库连接预算": 0.031, + "路由": 0.028, + "模块服务": 0.022, + "插件备份恢复": 0.021, "插件": 0.025, - "定时器": 0.022, + "定时器": 0.023, "监控器": 0.021, - "待处理整理回放": 0.024, - "命令服务": 0.022, - "工作流": 0.02, + "待处理整理回放": 0.025, + "命令服务": 0.021, + "工作流": 0.023, "插件同步与启动收尾": 0.024 }, "threads_before": 2, @@ -78,25 +78,25 @@ { "mode": "normal", "enabled_component_count": 25, - "startup_ms": 0.644, - "full_lifespan_ms": 1.465, + "startup_ms": 0.667, + "full_lifespan_ms": 1.559, "stage_ms": { - "后台任务登记器": 0.073, - "数据库准备": 0.035, - "HTTP 基础能力": 0.032, - "领域依赖装配": 0.029, - "数据库引擎预热": 0.025, - "数据库连接预算": 0.025, - "路由": 0.025, - "模块服务": 0.024, - "插件备份恢复": 0.023, - "插件": 0.021, - "定时器": 0.023, - "监控器": 0.025, - "待处理整理回放": 0.024, - "命令服务": 0.023, + "后台任务登记器": 0.078, + "数据库准备": 0.037, + "HTTP 基础能力": 0.027, + "领域依赖装配": 0.03, + "数据库引擎预热": 0.027, + "数据库连接预算": 0.024, + "路由": 0.026, + "模块服务": 0.026, + "插件备份恢复": 0.026, + "插件": 0.025, + "定时器": 0.024, + "监控器": 0.024, + "待处理整理回放": 0.027, + "命令服务": 0.026, "工作流": 0.023, - "插件同步与启动收尾": 0.02 + "插件同步与启动收尾": 0.021 }, "threads_before": 2, "threads_started": 2, @@ -109,25 +109,25 @@ { "mode": "normal", "enabled_component_count": 25, - "startup_ms": 0.661, - "full_lifespan_ms": 1.501, + "startup_ms": 0.735, + "full_lifespan_ms": 1.57, "stage_ms": { - "后台任务登记器": 0.073, - "数据库准备": 0.049, - "HTTP 基础能力": 0.032, - "领域依赖装配": 0.029, - "数据库引擎预热": 0.025, + "后台任务登记器": 0.103, + "数据库准备": 0.041, + "HTTP 基础能力": 0.031, + "领域依赖装配": 0.032, + "数据库引擎预热": 0.028, "数据库连接预算": 0.026, - "路由": 0.025, - "模块服务": 0.025, - "插件备份恢复": 0.023, + "路由": 0.027, + "模块服务": 0.026, + "插件备份恢复": 0.026, "插件": 0.025, - "定时器": 0.026, + "定时器": 0.024, "监控器": 0.025, - "待处理整理回放": 0.021, - "命令服务": 0.022, + "待处理整理回放": 0.024, + "命令服务": 0.024, "工作流": 0.024, - "插件同步与启动收尾": 0.021 + "插件同步与启动收尾": 0.024 }, "threads_before": 2, "threads_started": 2, @@ -138,8 +138,8 @@ "database_connections_started": 0 } ], - "median_startup_ms": 0.661, - "median_full_lifespan_ms": 1.475, + "median_startup_ms": 0.702, + "median_full_lifespan_ms": 1.559, "enabled_component_count": 25, "enabled_components": [ "后台任务登记器", @@ -174,41 +174,17 @@ { "mode": "safe", "enabled_component_count": 13, - "startup_ms": 0.48, - "full_lifespan_ms": 0.905, + "startup_ms": 0.527, + "full_lifespan_ms": 0.956, "stage_ms": { - "后台任务登记器": 0.073, - "数据库准备": 0.043, + "后台任务登记器": 0.091, + "数据库准备": 0.042, "HTTP 基础能力": 0.03, - "领域依赖装配": 0.027, - "数据库引擎预热": 0.026, - "数据库连接预算": 0.025, - "路由": 0.025, - "模块服务": 0.025, - "插件同步与启动收尾": 0.022 - }, - "threads_before": 2, - "threads_started": 2, - "threads_after": 2, - "tasks_before": 1, - "tasks_started": 1, - "tasks_after": 1, - "database_connections_started": 0 - }, - { - "mode": "safe", - "enabled_component_count": 13, - "startup_ms": 0.51, - "full_lifespan_ms": 0.935, - "stage_ms": { - "后台任务登记器": 0.082, - "数据库准备": 0.039, - "HTTP 基础能力": 0.036, - "领域依赖装配": 0.032, - "数据库引擎预热": 0.024, - "数据库连接预算": 0.026, + "领域依赖装配": 0.03, + "数据库引擎预热": 0.028, + "数据库连接预算": 0.027, "路由": 0.026, - "模块服务": 0.025, + "模块服务": 0.026, "插件同步与启动收尾": 0.025 }, "threads_before": 2, @@ -222,17 +198,41 @@ { "mode": "safe", "enabled_component_count": 13, - "startup_ms": 0.564, - "full_lifespan_ms": 0.97, + "startup_ms": 0.523, + "full_lifespan_ms": 0.954, "stage_ms": { - "后台任务登记器": 0.092, - "数据库准备": 0.039, + "后台任务登记器": 0.082, + "数据库准备": 0.045, "HTTP 基础能力": 0.036, - "领域依赖装配": 0.031, - "数据库引擎预热": 0.03, - "数据库连接预算": 0.026, - "路由": 0.027, - "模块服务": 0.027, + "领域依赖装配": 0.033, + "数据库引擎预热": 0.026, + "数据库连接预算": 0.024, + "路由": 0.023, + "模块服务": 0.024, + "插件同步与启动收尾": 0.023 + }, + "threads_before": 2, + "threads_started": 2, + "threads_after": 2, + "tasks_before": 1, + "tasks_started": 1, + "tasks_after": 1, + "database_connections_started": 0 + }, + { + "mode": "safe", + "enabled_component_count": 13, + "startup_ms": 0.515, + "full_lifespan_ms": 0.934, + "stage_ms": { + "后台任务登记器": 0.084, + "数据库准备": 0.044, + "HTTP 基础能力": 0.029, + "领域依赖装配": 0.027, + "数据库引擎预热": 0.031, + "数据库连接预算": 0.028, + "路由": 0.023, + "模块服务": 0.022, "插件同步与启动收尾": 0.025 }, "threads_before": 2, @@ -244,8 +244,8 @@ "database_connections_started": 0 } ], - "median_startup_ms": 0.51, - "median_full_lifespan_ms": 0.935, + "median_startup_ms": 0.523, + "median_full_lifespan_ms": 0.954, "enabled_component_count": 13, "enabled_components": [ "后台任务登记器", diff --git a/tests/test_architecture_adapter_imports.py b/tests/test_architecture_adapter_imports.py index 6dc128d69..ae92da7e5 100644 --- a/tests/test_architecture_adapter_imports.py +++ b/tests/test_architecture_adapter_imports.py @@ -37,7 +37,7 @@ FROZEN_DIRECT_ADAPTER_IMPORTS = { ("app.application.security.cookie", "app.adapters.network.http"): "S2-L6", ("app.application.security.passkey", "app.adapters.cache.redis"): "S2-L4", ("app.application.torrent", "app.adapters.network.http"): "S2-L6", - ("app.application.transfer", "app.adapters.system.host"): "S2-L6", + ("app.application.transfer.workflow", "app.adapters.system.host"): "S2-L6", ("app.chain._recognition", "app.adapters.external.server"): "S2-L7", ("app.chain._transfer", "app.adapters.system.host"): "S2-L7", ("app.chain.download", "app.adapters.network.http"): "S2-L7", diff --git a/tests/test_architecture_dependencies.py b/tests/test_architecture_dependencies.py index d9396c3a7..ff914f626 100644 --- a/tests/test_architecture_dependencies.py +++ b/tests/test_architecture_dependencies.py @@ -82,6 +82,13 @@ RETIRED_CANONICAL_FILES = ( "app/adapters/network/sites.pyi", "app/application/plugins.py", "app/application/subscribe.py", + "app/application/chain/durable_events.py", + "app/application/transfer.py", + "app/application/transfer_execution.py", + "app/db/adapters/transfer.py", + "app/db/adapters/transfer_execution.py", + "app/runtime/extensions/managed_resource_adapter.py", + "app/runtime/managed_resources.py", "app/startup/agent_initializer.py", "app/startup/cache_initializer.py", "app/startup/chain_events.py", @@ -93,6 +100,7 @@ RETIRED_CANONICAL_FILES = ( "app/startup/domain_initializer.py", "app/startup/download_failure.py", "app/startup/managed_resources_initializer.py", + "app/startup/initializers/managed_resources.py", "app/startup/modules_initializer.py", "app/startup/monitor_initializer.py", "app/startup/outbox.py", @@ -481,9 +489,9 @@ def test_transfer_chains_use_explicit_data_port_getters(): def test_transfer_pending_oper_import_is_confined_to_database_boundary(): """宿主仅允许事务适配器和兼容导出直接导入整理待处理 Oper。""" allowed_paths = { - "app/db/adapters/transfer.py", + "app/db/adapters/transfer/admission.py", "app/db/adapters/chain.py", - "app/db/adapters/transfer_execution.py", + "app/db/adapters/transfer/execution.py", "app/db/oper/__init__.py", } violations: list[str] = [] @@ -520,7 +528,7 @@ def test_startup_injects_transactional_transfer_admission_repository(): tree = ast.parse(path.read_text(encoding="utf-8-sig"), filename=str(path)) imports_repository = any( isinstance(node, ast.ImportFrom) - and node.module == "app.db.adapters.transfer" + and node.module == "app.db.adapters.transfer.admission" and any( alias.name == "TransactionalTransferAdmissionRepository" for alias in node.names diff --git a/tests/test_chain_durable_events.py b/tests/test_chain_durable_events.py index a516f3f43..4d540b8a4 100644 --- a/tests/test_chain_durable_events.py +++ b/tests/test_chain_durable_events.py @@ -11,7 +11,7 @@ from sqlalchemy import create_engine, delete, select from sqlalchemy.exc import IntegrityError from sqlalchemy.orm import sessionmaker -from app.application.chain.durable_events import ( +from app.application.chain.events import ( TransferResultSettlement, download_added_event_key, restore_download_added, @@ -21,7 +21,7 @@ from app.application.chain.durable_events import ( transfer_result_event_key, ) from app.application.history import TransferHistoryMutationCommand -from app.application.transfer_execution import ( +from app.application.transfer.execution import ( TransferExecutionLeaseLostError, TransferSettlementResult, ) diff --git a/tests/test_db_transferpending_queries.py b/tests/test_db_transferpending_queries.py index 942896378..2535b50d6 100644 --- a/tests/test_db_transferpending_queries.py +++ b/tests/test_db_transferpending_queries.py @@ -12,7 +12,7 @@ from sqlalchemy import create_engine, select from sqlalchemy.orm import sessionmaker from app.db import base as db_base -from app.db.adapters.transfer import TransactionalTransferAdmissionRepository +from app.db.adapters.transfer.admission import TransactionalTransferAdmissionRepository from app.db.models.transferhistory import TransferHistory from app.db.models.transferpending import TransferPending from app.db.oper.transferpending import TransferPendingOper diff --git a/tests/test_filemanager_planning.py b/tests/test_filemanager_planning.py index bdd610281..9785be525 100644 --- a/tests/test_filemanager_planning.py +++ b/tests/test_filemanager_planning.py @@ -3,7 +3,7 @@ from pathlib import Path import pytest -from app.application.transfer import TransferPlanCheckpoint, TransferPlanningInput +from app.application.transfer.workflow import TransferPlanCheckpoint, TransferPlanningInput from app.domain.context import MediaInfo from app.domain.meta.metabase import MetaBase from app.modules.filemanager import transhandler as transhandler_module diff --git a/tests/test_history_ai_retry_gate.py b/tests/test_history_ai_retry_gate.py index fdcac2bf3..c858ca903 100644 --- a/tests/test_history_ai_retry_gate.py +++ b/tests/test_history_ai_retry_gate.py @@ -8,7 +8,7 @@ from types import SimpleNamespace from app.agent.tools.impl.delete_transfer_history import DeleteTransferHistoryTool from app.api.endpoints import history as history_endpoint from app.application.configuration import ApiRuntimeConfig -from app.application.transfer_execution import ( +from app.application.transfer.execution import ( TransferExecutionState, TransferRetryRequestResult, ) diff --git a/tests/test_legacy_db_behavior_compat.py b/tests/test_legacy_db_behavior_compat.py index 938c930e7..ccbedf474 100644 --- a/tests/test_legacy_db_behavior_compat.py +++ b/tests/test_legacy_db_behavior_compat.py @@ -2,7 +2,7 @@ import importlib import pytest -from app.application.transfer import TransferTask as CanonicalTransferTask +from app.application.transfer.workflow import TransferTask as CanonicalTransferTask from app.db.models.transferhistory import TransferHistory from app.schemas.file import FileItem diff --git a/tests/test_legacy_import_compat.py b/tests/test_legacy_import_compat.py index 2dc7eee88..9cf25d2e4 100644 --- a/tests/test_legacy_import_compat.py +++ b/tests/test_legacy_import_compat.py @@ -189,6 +189,37 @@ for legacy_name, alias in MODULE_ALIASES.items(): ) +def test_renamed_modules_use_exact_compat_routes() -> None: + """旧模块路径只经 manifest 复用新的 canonical 模块。""" + expected = { + "app.application.chain.durable_events": "app.application.chain.events", + "app.application.transfer_execution": "app.application.transfer.execution", + "app.runtime.managed_resources": "app.runtime.resources", + } + configure_legacy_import_diagnostics(enabled=False, emitter=lambda _: None) + try: + for legacy_name, target_name in expected.items(): + assert MODULE_ALIASES[legacy_name].target == target_name + assert importlib.import_module(legacy_name) is importlib.import_module(target_name) + finally: + reset_legacy_import_diagnostics() + + +def test_transfer_package_exposes_plugin_symbols_only_through_overlay() -> None: + """整理包不重导出宿主实现,只按兼容清单惰性提供插件旧符号。""" + configure_legacy_import_diagnostics(enabled=False, emitter=lambda _: None) + try: + package = importlib.import_module("app.application.transfer") + legacy = importlib.import_module("app.sdk._legacy.transfer") + + assert package.TransferTask is legacy.TransferTask + assert package.TransferQueue is legacy.TransferQueue + assert "TransferTask" not in package.__all__ + assert "TransferQueue" not in package.__all__ + finally: + reset_legacy_import_diagnostics() + + def test_virtual_package_exports_resolve_exact_manifest_symbols(): """合成旧包仅公开 manifest 声明的符号,并记录 DEBUG 兼容警告。""" legacy_package = "app.core.meta" diff --git a/tests/test_managed_resources.py b/tests/test_managed_resources.py index 80694fab8..d976b2620 100644 --- a/tests/test_managed_resources.py +++ b/tests/test_managed_resources.py @@ -13,18 +13,18 @@ from unittest.mock import MagicMock import pytest +from app.runtime import resources as managed_resource_facade from app.runtime.capabilities.errors import ( CapabilityOperationError, CapabilityRuntimeClosedError, ) from app.runtime.capabilities.runtime import CapabilityRuntime -from app.runtime.extensions.managed_resource_adapter import ( +from app.runtime.extensions.resource import ( AsyncManagedResourceAdapter, SyncManagedResourceAdapter, build_managed_resource_registry, ) -from app.runtime import managed_resources as managed_resource_facade -from app.runtime.managed_resources import ( +from app.runtime.resources import ( MANAGED_RESOURCE_ASYNC_KIND, MANAGED_RESOURCE_SYNC_KIND, acquire_managed_resource, @@ -35,7 +35,6 @@ from app.runtime.managed_resources import ( shutdown_managed_resource_runtime, ) - PROJECT_ROOT = Path(__file__).parents[1] @@ -346,7 +345,7 @@ def test_startup_initializer_discovers_manifest_without_importing_resource() -> script = """ import asyncio import sys -from app.startup.initializers.managed_resources import ( +from app.startup.initializers.resources import ( init_managed_resources, stop_managed_resources, ) @@ -372,7 +371,7 @@ assert "pyvirtualdisplay" not in sys.modules def test_startup_shutdown_without_init_does_not_build_registry(monkeypatch) -> None: """未执行启动装配时,关闭入口不得通过发现声明反向初始化 Runtime。""" - from app.startup.initializers import managed_resources as managed_resources_initializer + from app.startup.initializers import resources as managed_resources_initializer build_registry = MagicMock(side_effect=AssertionError("must not discover")) monkeypatch.setattr( diff --git a/tests/test_manual_transfer_history.py b/tests/test_manual_transfer_history.py index a97986c8b..a30621a72 100644 --- a/tests/test_manual_transfer_history.py +++ b/tests/test_manual_transfer_history.py @@ -2,18 +2,20 @@ from types import SimpleNamespace from app.api.endpoints.transfer import ( manual_transfer as manual_transfer_endpoint, +) +from app.api.endpoints.transfer import ( query_manual_transfer_history, ) -from app.chain.transfer import TransferChain -from app.runtime.config import settings -from app.db.oper.transferhistory import TransferHistoryOper from app.application.history import ( clear_transfer_failures, failed_retry_count, max_failed_retries, record_transfer_failure, ) -from app.schemas import ManualTransferItem +from app.chain.transfer import TransferChain +from app.db.oper.transferhistory import TransferHistoryOper +from app.runtime.config import settings +from app.schemas.transfer import ManualTransferItem from tests.test_transfer_sync_extra_files import ( FakeMeta, make_fileitem, @@ -72,12 +74,6 @@ def _patch_transfer_planning(monkeypatch, chain, fileitem, history, planned, del get_by_path=lambda path: None, ), ) - monkeypatch.setattr("app.chain._transfer.get_chain_download_history_port", lambda: SimpleNamespace( - get_by_hash=lambda download_hash: None, - get_file_by_fullpath=lambda fullpath: None, - get_files_by_savepath=lambda savepath: [], - get_by_path=lambda path: None, - )) monkeypatch.setattr( "app.chain.transfer.get_configured_system_config", lambda: SimpleNamespace(get=lambda key: None), diff --git a/tests/test_music_transfer.py b/tests/test_music_transfer.py index 98fafde71..36649e6c9 100644 --- a/tests/test_music_transfer.py +++ b/tests/test_music_transfer.py @@ -5,7 +5,7 @@ from unittest.mock import Mock from jinja2 import Template from app.application.messaging.message import TemplateHelper -from app.application.transfer import TransferTask +from app.application.transfer.workflow import TransferTask from app.chain.media import MediaChain from app.chain.transfer import JobManager, TransferChain from app.domain.context import MusicInfo diff --git a/tests/test_transfer_durable_retry_owner.py b/tests/test_transfer_durable_retry_owner.py index e5a33000d..5d10e02cb 100644 --- a/tests/test_transfer_durable_retry_owner.py +++ b/tests/test_transfer_durable_retry_owner.py @@ -2,7 +2,7 @@ from types import SimpleNamespace -from app.application.transfer_execution import ( +from app.application.transfer.execution import ( TransferExecutionState, TransferRetryRequestResult, ) diff --git a/tests/test_transfer_execution_migration.py b/tests/test_transfer_execution_migration.py index a45d5466e..1a7deb3df 100644 --- a/tests/test_transfer_execution_migration.py +++ b/tests/test_transfer_execution_migration.py @@ -11,7 +11,7 @@ from alembic.migration import MigrationContext from alembic.operations import Operations from sqlalchemy.orm import sessionmaker -from app.application.transfer_execution import ( +from app.application.transfer.execution import ( TransferExecutionCommand, TransferExecutionConflictError, TransferExecutionState, @@ -20,7 +20,7 @@ from app.application.transfer_execution import ( TransferStepIntent, TransferStepResult, ) -from app.db.adapters.transfer_execution import ( +from app.db.adapters.transfer.execution import ( TransactionalTransferExecutionRepository, ) from app.db.models.transferpending import TransferPending diff --git a/tests/test_transfer_execution_persistence.py b/tests/test_transfer_execution_persistence.py index 1249ee570..1247dd910 100644 --- a/tests/test_transfer_execution_persistence.py +++ b/tests/test_transfer_execution_persistence.py @@ -6,7 +6,7 @@ import pytest from sqlalchemy import create_engine, select from sqlalchemy.orm import sessionmaker -from app.application.transfer_execution import ( +from app.application.transfer.execution import ( TransferExecutionCheckpoint, TransferExecutionCommand, TransferExecutionConflictError, @@ -20,7 +20,7 @@ from app.application.transfer_execution import ( build_transfer_checkpoint_fingerprint, build_transfer_operation_id, ) -from app.db.adapters.transfer_execution import ( +from app.db.adapters.transfer.execution import ( TransactionalTransferExecutionRepository, ) from app.db.base import Base diff --git a/tests/test_transfer_execution_runner.py b/tests/test_transfer_execution_runner.py index e9f8637d6..74bfc37f0 100644 --- a/tests/test_transfer_execution_runner.py +++ b/tests/test_transfer_execution_runner.py @@ -8,7 +8,7 @@ import pytest from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker -from app.application.transfer_execution import ( +from app.application.transfer.execution import ( TransferExecutionCommand, TransferExecutionState, TransferOperationObservation, @@ -17,7 +17,7 @@ from app.application.transfer_execution import ( TransferStepResult, ) from app.chain import transfer as transfer_chain_module -from app.db.adapters.transfer_execution import ( +from app.db.adapters.transfer.execution import ( TransactionalTransferExecutionRepository, ) from app.db.base import Base diff --git a/tests/test_transfer_failed_retry_scheduler.py b/tests/test_transfer_failed_retry_scheduler.py index a273cda0c..4fb794fd2 100644 --- a/tests/test_transfer_failed_retry_scheduler.py +++ b/tests/test_transfer_failed_retry_scheduler.py @@ -5,7 +5,7 @@ from unittest.mock import Mock, patch import pytest -from app.application.transfer import FailedRetryScheduler +from app.application.transfer.workflow import FailedRetryScheduler def test_retry_scheduler_close_cancels_buffered_timer_and_rejects_new_work(): @@ -76,7 +76,7 @@ def test_retry_scheduler_observes_unexpected_background_task_error(): raise RuntimeError("flush failed") scheduler._flush_retry_transfer = failing_flush - with patch("app.application.transfer.logger.error", Mock()) as log_error: + with patch("app.application.transfer.workflow.logger.error", Mock()) as log_error: await scheduler.schedule_retry(11, group_key="media:test") for _ in range(5): await asyncio.sleep(0) diff --git a/tests/test_transfer_failure_notification_aggregation.py b/tests/test_transfer_failure_notification_aggregation.py index c19415389..0449372b6 100644 --- a/tests/test_transfer_failure_notification_aggregation.py +++ b/tests/test_transfer_failure_notification_aggregation.py @@ -1,16 +1,16 @@ -from unittest.mock import Mock, patch from types import SimpleNamespace +from unittest.mock import Mock, patch import pytest -from app.chain import transfer as transfer_module -from app.chain.transfer import TransferChain -from app.application.transfer import ( +from app.application.transfer.workflow import ( TransferFailureNotification, TransferFailureNotificationAggregator, TransferTask, build_transfer_failure_group_key, ) +from app.chain import transfer as transfer_module +from app.chain.transfer import TransferChain from app.domain.context import MediaInfo from app.domain.metainfo import MetaInfo from app.runtime.config import ConfigModel @@ -247,7 +247,7 @@ def test_aggregator_close_observes_flush_callback_error(): loop=loop, ) - with patch("app.application.transfer.logger.error") as log_error: + with patch("app.application.transfer.workflow.logger.error") as log_error: aggregator.close() callback.assert_called_once_with([notice]) diff --git a/tests/test_transfer_job_manager.py b/tests/test_transfer_job_manager.py index 9c973ad0e..0b2b142e3 100644 --- a/tests/test_transfer_job_manager.py +++ b/tests/test_transfer_job_manager.py @@ -9,7 +9,7 @@ from app.application.history import ( failed_retry_count, record_transfer_failure, ) -from app.application.transfer import ( +from app.application.transfer.workflow import ( TransferAdmission, TransferPlanningInput, TransferTask, diff --git a/tests/test_transfer_lease_persistence.py b/tests/test_transfer_lease_persistence.py index f45c73396..b63c99a38 100644 --- a/tests/test_transfer_lease_persistence.py +++ b/tests/test_transfer_lease_persistence.py @@ -9,7 +9,7 @@ import pytest from sqlalchemy import create_engine, select, text from sqlalchemy.orm import sessionmaker -from app.application.transfer import ( +from app.application.transfer.workflow import ( TRANSFER_ADMISSION_PLANNED, TransferAdmission, TransferAdmissionProjectionError, @@ -19,7 +19,7 @@ from app.application.transfer import ( TransferProviderInvocationSnapshot, TransferProviderReference, ) -from app.db.adapters.transfer import TransactionalTransferAdmissionRepository +from app.db.adapters.transfer.admission import TransactionalTransferAdmissionRepository from app.db.models.transferhistory import TransferHistory from app.db.models.transferpending import TransferPending from app.db.oper.transferpending import TransferPendingOper @@ -321,7 +321,7 @@ def test_claim_recoverable_skips_corrupt_projection_and_claims_later_tasks( repository = repository_factory() messages: list[str] = [] monkeypatch.setattr( - "app.db.adapters.transfer._diagnostic_logger.error", + "app.db.adapters.transfer.admission._diagnostic_logger.error", messages.append, ) corrupt = _admit(repository, "/downloads/a-corrupt.mkv") @@ -389,7 +389,7 @@ def test_projection_diagnostic_changes_are_recorded_once_each( repository = repository_factory() messages: list[str] = [] monkeypatch.setattr( - "app.db.adapters.transfer._diagnostic_logger.error", + "app.db.adapters.transfer.admission._diagnostic_logger.error", messages.append, ) admitted = _admit(repository, "/downloads/changing-corrupt.mkv") @@ -459,7 +459,7 @@ def test_projection_diagnostic_cas_is_concurrency_safe( messages.append(message) monkeypatch.setattr( - "app.db.adapters.transfer._diagnostic_logger.error", + "app.db.adapters.transfer.admission._diagnostic_logger.error", capture, ) barrier = Barrier(2) @@ -498,7 +498,7 @@ def test_projection_diagnostic_does_not_overwrite_active_lease( before = _pending_snapshot(repository, admitted.task_id) messages: list[str] = [] monkeypatch.setattr( - "app.db.adapters.transfer._diagnostic_logger.error", + "app.db.adapters.transfer.admission._diagnostic_logger.error", messages.append, ) diff --git a/tests/test_transfer_legacy_terminal_compat.py b/tests/test_transfer_legacy_terminal_compat.py index cf90f8e57..8a0c3488f 100644 --- a/tests/test_transfer_legacy_terminal_compat.py +++ b/tests/test_transfer_legacy_terminal_compat.py @@ -4,7 +4,7 @@ import threading from types import SimpleNamespace from unittest.mock import Mock, patch -from app.application.transfer_execution import ( +from app.application.transfer.execution import ( TransferExecutionCheckpoint, TransferSettlementResult, ) diff --git a/tests/test_transfer_manual_review_api.py b/tests/test_transfer_manual_review_api.py index ed2342f98..111c5b5d1 100644 --- a/tests/test_transfer_manual_review_api.py +++ b/tests/test_transfer_manual_review_api.py @@ -11,7 +11,7 @@ from pydantic import ValidationError from app.api.dependencies.auth import get_current_active_manage_user from app.api.endpoints import transfer as transfer_endpoint -from app.application.transfer_execution import ( +from app.application.transfer.execution import ( TransferExecutionConflictError, TransferExecutionState, TransferManualReviewDecision, diff --git a/tests/test_transfer_manual_review_discovery.py b/tests/test_transfer_manual_review_discovery.py index 6d9011602..00a722ff3 100644 --- a/tests/test_transfer_manual_review_discovery.py +++ b/tests/test_transfer_manual_review_discovery.py @@ -8,14 +8,14 @@ from sqlalchemy import create_engine, select from sqlalchemy.orm import sessionmaker from app.api.endpoints import transfer as transfer_endpoint -from app.application.transfer_execution import ( +from app.application.transfer.execution import ( TransferExecutionCommand, TransferExecutionState, TransferManualReviewQuery, TransferStepIntent, TransferStepResult, ) -from app.db.adapters.transfer_execution import ( +from app.db.adapters.transfer.execution import ( TransactionalTransferExecutionRepository, ) from app.db.base import Base diff --git a/tests/test_transfer_mark_torrent_completed.py b/tests/test_transfer_mark_torrent_completed.py index cff7ca63b..ecc7731e4 100644 --- a/tests/test_transfer_mark_torrent_completed.py +++ b/tests/test_transfer_mark_torrent_completed.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- from types import SimpleNamespace +from app.application.transfer.workflow import TransferTask from app.chain.transfer import JobManager, TransferChain from app.domain.meta.metabase import MetaBase from app.runtime.config import settings -from app.schemas import FileItem -from app.application.transfer import TransferTask +from app.schemas.file import FileItem from app.schemas.types import MediaType diff --git a/tests/test_transfer_mounted_disk_cleanup.py b/tests/test_transfer_mounted_disk_cleanup.py index ad11c0629..ff96a9a43 100644 --- a/tests/test_transfer_mounted_disk_cleanup.py +++ b/tests/test_transfer_mounted_disk_cleanup.py @@ -2,10 +2,11 @@ from pathlib import Path from types import SimpleNamespace from unittest.mock import patch -from app.chain.transfer import TransferChain -from app.schemas import FileItem, TransferDirectoryConf -from app.application.transfer import TransferTask from app.adapters.system.host import SystemUtils +from app.application.transfer.workflow import TransferTask +from app.chain.transfer import TransferChain +from app.schemas.file import FileItem +from app.schemas.system import TransferDirectoryConf def _make_task( diff --git a/tests/test_transfer_movie_collection.py b/tests/test_transfer_movie_collection.py index 523cccfa7..7c1ed1a3a 100644 --- a/tests/test_transfer_movie_collection.py +++ b/tests/test_transfer_movie_collection.py @@ -2,12 +2,13 @@ from types import SimpleNamespace import pytest +from app.application.transfer.workflow import TransferTask from app.chain.transfer import TransferChain -from app.runtime.config import settings from app.domain.context import MediaInfo from app.domain.meta.metabase import MetaBase -from app.schemas import DownloadHistory, FileItem -from app.application.transfer import TransferTask +from app.runtime.config import settings +from app.schemas.file import FileItem +from app.schemas.history import DownloadHistory from app.schemas.types import MediaType @@ -192,7 +193,6 @@ def test_movie_collection_conflict_only_drops_automatic_media( ) monkeypatch.setattr("app.chain._transfer.get_chain_transfer_history_port", lambda: SimpleNamespace(get_by_src=lambda src, storage=None: None)) monkeypatch.setattr("app.chain.transfer.get_chain_download_history_port", lambda: history_oper) - monkeypatch.setattr("app.chain._transfer.get_chain_download_history_port", lambda: history_oper) monkeypatch.setattr( "app.chain.transfer.get_configured_system_config", lambda: SimpleNamespace(get=lambda key: None), diff --git a/tests/test_transfer_music_job_media.py b/tests/test_transfer_music_job_media.py index c15f3eb54..676a06b98 100644 --- a/tests/test_transfer_music_job_media.py +++ b/tests/test_transfer_music_job_media.py @@ -1,8 +1,8 @@ from app import schemas -from app.application.transfer import TransferTask +from app.application.transfer.workflow import TransferTask from app.chain.transfer import JobManager from app.domain.meta.metamusic import MetaMusic -from app.schemas import FileItem +from app.schemas.file import FileItem def _music_task() -> TransferTask: diff --git a/tests/test_transfer_overwrite_declined.py b/tests/test_transfer_overwrite_declined.py index c69cba4bc..9f3a28aa0 100644 --- a/tests/test_transfer_overwrite_declined.py +++ b/tests/test_transfer_overwrite_declined.py @@ -11,7 +11,7 @@ from unittest.mock import MagicMock, patch import pytest -from app.application.transfer_execution import ( +from app.application.transfer.execution import ( TransferExecutionCheckpoint, TransferSettlementResult, ) diff --git a/tests/test_transfer_pending_legacy_compat.py b/tests/test_transfer_pending_legacy_compat.py index 7a5de43d7..cc5037f04 100644 --- a/tests/test_transfer_pending_legacy_compat.py +++ b/tests/test_transfer_pending_legacy_compat.py @@ -50,7 +50,7 @@ def test_legacy_import_targets_private_sdk_facade() -> None: assert alias.target == "app.sdk._legacy.transferpending" assert alias.owner == "sdk" - assert alias.replacement == "app.application.transfer" + assert alias.replacement == "app.application.transfer.workflow" assert legacy is importlib.import_module(alias.target) assert legacy.__all__ == ["TransferPendingOper"] assert not hasattr(legacy, "TransferPending") diff --git a/tests/test_transfer_pending_replay.py b/tests/test_transfer_pending_replay.py index 6108e5dde..1b55e50ef 100644 --- a/tests/test_transfer_pending_replay.py +++ b/tests/test_transfer_pending_replay.py @@ -13,7 +13,7 @@ from dataclasses import replace from pathlib import Path from unittest.mock import MagicMock -from app.application.transfer import TransferAdmission, TransferPlanningInput, TransferTask +from app.application.transfer.workflow import TransferAdmission, TransferPlanningInput, TransferTask from app.chain.transfer import TransferChain from app.schemas.file import FileItem diff --git a/tests/test_transfer_planning_checkpoint.py b/tests/test_transfer_planning_checkpoint.py index 83948261a..c25873d3c 100644 --- a/tests/test_transfer_planning_checkpoint.py +++ b/tests/test_transfer_planning_checkpoint.py @@ -12,14 +12,14 @@ import pytest from sqlalchemy import create_engine, select from sqlalchemy.orm import sessionmaker -from app.application import transfer as transfer_application -from app.application.transfer import TransferTask -from app.application.transfer_execution import ( +from app.application.transfer import workflow as transfer_application +from app.application.transfer.execution import ( TransferExecutionCheckpoint, TransferSettlementResult, ) +from app.application.transfer.workflow import TransferTask from app.chain.transfer import TransferChain -from app.db.adapters.transfer import TransactionalTransferAdmissionRepository +from app.db.adapters.transfer.admission import TransactionalTransferAdmissionRepository from app.db.models.transferhistory import TransferHistory from app.db.models.transferpending import TransferPending from app.domain.context import MediaInfo diff --git a/tests/test_transfer_planning_compat.py b/tests/test_transfer_planning_compat.py index 7b653dcee..86f5571d9 100644 --- a/tests/test_transfer_planning_compat.py +++ b/tests/test_transfer_planning_compat.py @@ -6,7 +6,7 @@ from unittest.mock import Mock import pytest -from app.application.transfer import TransferTask +from app.application.transfer.workflow import TransferTask from app.chain import ChainBase from app.chain.transfer import TransferChain from app.modules.filemanager.module import FileManagerModule diff --git a/tests/test_transfer_planning_migration.py b/tests/test_transfer_planning_migration.py index f37a1e5cd..3997db772 100644 --- a/tests/test_transfer_planning_migration.py +++ b/tests/test_transfer_planning_migration.py @@ -10,7 +10,7 @@ import sqlalchemy as sa from alembic.migration import MigrationContext from alembic.operations import Operations -from app.application.transfer import TransferPlanningInput +from app.application.transfer.workflow import TransferPlanningInput from app.db.models.transferpending import TransferPending try: diff --git a/tests/test_transfer_planning_persistence.py b/tests/test_transfer_planning_persistence.py index 6df131c74..8bdecb4da 100644 --- a/tests/test_transfer_planning_persistence.py +++ b/tests/test_transfer_planning_persistence.py @@ -6,7 +6,7 @@ import pytest from sqlalchemy import create_engine, select from sqlalchemy.orm import sessionmaker -from app.application.transfer import ( +from app.application.transfer.workflow import ( TRANSFER_ADMISSION_ACCEPTED, TRANSFER_ADMISSION_PLANNED, TRANSFER_ADMISSION_PROVIDER_PENDING, @@ -19,7 +19,7 @@ from app.application.transfer import ( TransferProviderInvocationSnapshot, TransferProviderReference, ) -from app.db.adapters.transfer import TransactionalTransferAdmissionRepository +from app.db.adapters.transfer.admission import TransactionalTransferAdmissionRepository from app.db.models.transferhistory import TransferHistory from app.db.models.transferpending import TransferPending diff --git a/tests/test_transfer_queue_service.py b/tests/test_transfer_queue_service.py index cdc8bb547..0c9fdbcfe 100644 --- a/tests/test_transfer_queue_service.py +++ b/tests/test_transfer_queue_service.py @@ -5,8 +5,8 @@ import pytest from sqlalchemy import create_engine, select from sqlalchemy.orm import sessionmaker -from app.application.transfer import TransferAdmission, TransferQueueService -from app.db.adapters.transfer import TransactionalTransferAdmissionRepository +from app.application.transfer.workflow import TransferAdmission, TransferQueueService +from app.db.adapters.transfer.admission import TransactionalTransferAdmissionRepository from app.db.models.transferhistory import TransferHistory from app.db.models.transferpending import TransferPending from app.schemas.file import FileItem diff --git a/tests/test_transfer_settling_recovery.py b/tests/test_transfer_settling_recovery.py index a29669d36..33f7d219d 100644 --- a/tests/test_transfer_settling_recovery.py +++ b/tests/test_transfer_settling_recovery.py @@ -9,19 +9,19 @@ import pytest from sqlalchemy import create_engine, select from sqlalchemy.orm import sessionmaker -from app.application.transfer import ( +from app.application.transfer.execution import ( + TransferExecutionCheckpoint, + TransferExecutionSnapshot, + TransferExecutionState, +) +from app.application.transfer.workflow import ( TransferAdmission, TransferPlanCheckpoint, TransferPlanningInput, TransferTask, ) -from app.application.transfer_execution import ( - TransferExecutionCheckpoint, - TransferExecutionSnapshot, - TransferExecutionState, -) from app.chain.transfer import TransferChain -from app.db.adapters.transfer import TransactionalTransferAdmissionRepository +from app.db.adapters.transfer.admission import TransactionalTransferAdmissionRepository from app.db.models.transferhistory import TransferHistory from app.db.models.transferpending import TransferPending from app.schemas.file import FileItem diff --git a/tests/test_transfer_stale_tasks.py b/tests/test_transfer_stale_tasks.py index 90210bb0d..6da22a3e5 100644 --- a/tests/test_transfer_stale_tasks.py +++ b/tests/test_transfer_stale_tasks.py @@ -1,10 +1,10 @@ """整理任务失活收敛行为测试。""" +from app.application.transfer import workflow as app_transfer +from app.application.transfer.workflow import TransferTask from app.chain.transfer import JobManager from app.domain.meta.metabase import MetaBase -from app.schemas import FileItem -from app.application.transfer import TransferTask -from app.application import transfer as app_transfer +from app.schemas.file import FileItem from app.schemas.types import MediaType diff --git a/tests/test_transfer_sync_extra_files.py b/tests/test_transfer_sync_extra_files.py index 3f1de01c5..0c9947066 100644 --- a/tests/test_transfer_sync_extra_files.py +++ b/tests/test_transfer_sync_extra_files.py @@ -148,12 +148,6 @@ def test_sync_extra_subtitle_inherits_matching_video_episode(monkeypatch): get_by_path=lambda path: None, ), ) - monkeypatch.setattr("app.chain._transfer.get_chain_download_history_port", lambda: SimpleNamespace( - get_by_hash=lambda download_hash: None, - get_file_by_fullpath=lambda fullpath: None, - get_files_by_savepath=lambda savepath: [], - get_by_path=lambda path: None, - )) monkeypatch.setattr( "app.chain.transfer.get_configured_system_config", lambda: SimpleNamespace(get=lambda key: None), @@ -245,12 +239,6 @@ def test_single_subtitle_transfer_reuses_same_name_video_episode(monkeypatch): get_by_path=lambda path: None, ), ) - monkeypatch.setattr("app.chain._transfer.get_chain_download_history_port", lambda: SimpleNamespace( - get_by_hash=lambda download_hash: None, - get_file_by_fullpath=lambda fullpath: None, - get_files_by_savepath=lambda savepath: [], - get_by_path=lambda path: None, - )) monkeypatch.setattr( "app.chain.transfer.get_configured_system_config", lambda: SimpleNamespace(get=lambda key: None), @@ -360,12 +348,6 @@ def test_single_video_transfer_lists_parent_once_for_same_name_extra(monkeypatch get_by_path=lambda path: None, ), ) - monkeypatch.setattr("app.chain._transfer.get_chain_download_history_port", lambda: SimpleNamespace( - get_by_hash=lambda download_hash: None, - get_file_by_fullpath=lambda fullpath: None, - get_files_by_savepath=lambda savepath: [], - get_by_path=lambda path: None, - )) monkeypatch.setattr( "app.chain.transfer.get_configured_system_config", lambda: SimpleNamespace(get=lambda key: None), @@ -458,12 +440,6 @@ def test_episode_format_filters_extra_files_before_sync_planning(monkeypatch): get_by_path=lambda path: None, ), ) - monkeypatch.setattr("app.chain._transfer.get_chain_download_history_port", lambda: SimpleNamespace( - get_by_hash=lambda download_hash: None, - get_file_by_fullpath=lambda fullpath: None, - get_files_by_savepath=lambda savepath: [], - get_by_path=lambda path: None, - )) monkeypatch.setattr( "app.chain.transfer.get_configured_system_config", lambda: SimpleNamespace(get=lambda key: None), @@ -545,12 +521,6 @@ def test_episode_format_keeps_matching_extra_files_following_main(monkeypatch): get_by_path=lambda path: None, ), ) - monkeypatch.setattr("app.chain._transfer.get_chain_download_history_port", lambda: SimpleNamespace( - get_by_hash=lambda download_hash: None, - get_file_by_fullpath=lambda fullpath: None, - get_files_by_savepath=lambda savepath: [], - get_by_path=lambda path: None, - )) monkeypatch.setattr( "app.chain.transfer.get_configured_system_config", lambda: SimpleNamespace(get=lambda key: None), @@ -641,12 +611,6 @@ def test_single_matching_subtitle_uses_unmatched_video_only_as_context(monkeypat get_by_path=lambda path: None, ), ) - monkeypatch.setattr("app.chain._transfer.get_chain_download_history_port", lambda: SimpleNamespace( - get_by_hash=lambda download_hash: None, - get_file_by_fullpath=lambda fullpath: None, - get_files_by_savepath=lambda savepath: [], - get_by_path=lambda path: None, - )) monkeypatch.setattr( "app.chain.transfer.get_configured_system_config", lambda: SimpleNamespace(get=lambda key: None), @@ -741,12 +705,6 @@ def test_cleanup_dest_fileitem_is_checkpointed_only_after_allowed_items_exist(mo get_by_path=lambda path: None, ), ) - monkeypatch.setattr("app.chain._transfer.get_chain_download_history_port", lambda: SimpleNamespace( - get_by_hash=lambda download_hash: None, - get_file_by_fullpath=lambda fullpath: None, - get_files_by_savepath=lambda savepath: [], - get_by_path=lambda path: None, - )) monkeypatch.setattr( "app.chain.transfer.get_configured_system_config", lambda: SimpleNamespace(get=lambda key: None), diff --git a/tests/test_transfer_tmdb_category.py b/tests/test_transfer_tmdb_category.py index 1d74228f0..045caa7d4 100644 --- a/tests/test_transfer_tmdb_category.py +++ b/tests/test_transfer_tmdb_category.py @@ -1,10 +1,11 @@ from types import SimpleNamespace +from app.application.transfer.workflow import TransferTask from app.chain.transfer import TransferChain from app.domain.context import MediaInfo from app.domain.metainfo import MetaInfo -from app.schemas import FileItem, TransferDirectoryConf -from app.application.transfer import TransferTask +from app.schemas.file import FileItem +from app.schemas.system import TransferDirectoryConf from app.schemas.types import MediaSource, MediaType diff --git a/tests/test_transfer_worker_lifecycle.py b/tests/test_transfer_worker_lifecycle.py index 8ddc6af22..c89cb52a0 100644 --- a/tests/test_transfer_worker_lifecycle.py +++ b/tests/test_transfer_worker_lifecycle.py @@ -10,7 +10,7 @@ from unittest.mock import AsyncMock, MagicMock, patch import pytest -from app.application.transfer import TransferAdmission, TransferQueue, TransferTask +from app.application.transfer.workflow import TransferAdmission, TransferQueue, TransferTask from app.chain.transfer import TransferChain from app.foundation.singleton import Singleton from app.runtime.config import global_vars diff --git a/tests/test_transhandler_special_extra.py b/tests/test_transhandler_special_extra.py index d74faf6b3..f2bbef999 100644 --- a/tests/test_transhandler_special_extra.py +++ b/tests/test_transhandler_special_extra.py @@ -3,7 +3,7 @@ from unittest.mock import MagicMock import pytest -from app.application.transfer import TransferPlanningInput +from app.application.transfer.workflow import TransferPlanningInput from app.domain.context import MediaInfo from app.domain.meta.metavideo import MetaVideo from app.modules.filemanager.transhandler import TransHandler