refactor: reorganize startup persistence boundaries

This commit is contained in:
jxxghp
2026-08-23 21:24:34 +08:00
parent 7f09927c47
commit e1509c4e0d
96 changed files with 1036 additions and 782 deletions
+11 -8
View File
@@ -66,11 +66,13 @@ The legacy roots have no physical directories in the source tree. Current images
| `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` |
| `app/adapters/external/` | CookieCloud、插件市场、OCR、IP 归属和 MoviePilot Server 等命名外部生态 | 通用 HTTP/DNS/文件机制或可复用领域语义 | `market.py`, `server.py`, `cookiecloud.py`, `ocr.py`, `location.py`, `wechat_crypt.py` |
| `app/application/` | 读取配置/持久化状态的聚焦应用服务和服务族规则 | 多领域 Chain 编排、底层通用机制、通用传输协议 | `recognition.py`, `filter.py`, `filter_rules.py`, `notification.py`, `mediaserver.py`, `rss.py`, `site/sites.*` |
| `app/application/` | 聚焦应用服务、用例命令,以及由用例拥有的持久化 Port/Protocol | SQLAlchemy、Session、Oper 等具体 DB 实现,多领域 Chain 编排、底层通用机制、通用传输协议 | `recognition.py`, `filter.py`, `outbox.py`, `subscription/write.py`, `workflow.py` |
| `app/application/messaging/` | 消息渲染/路由、交互和 Agent 到消息桥接:`interaction.py` 通用交互契约和视图工具;`router.py` 统一交互优先级和回调分发;`site.py`/`subscribe.py`/`skill.py` 对应命令的会话、输入解析和视图;`media.py` 媒体交互状态(业务工作流仍由 `MediaInteractionChain` 执行);`plugin.py` 插件输入接管和插件按钮回调;`agent.py` Agent 选择状态、回调协议和 WebAgent 消息桥接;`message.py` 通知渲染、模板和队列。不作为推荐给插件直接使用的公开 SDK | 认证策略、通用 HTTP、服务发现、仅端点使用的 Web Push 行为 | `message.py`, `interaction.py`, `router.py`, `agent.py` |
| `app/application/security/` | 认证、授权、Cookie、Passkey、OTP/二次认证、路径/URL 安全、SSRF 和签名策略 | 通用 URL 解析、进程运行策略、普通业务校验 | `access.py`, `auth.py`, `cookie.py`, `passkey.py`, `otp.py`, `twofactor.py`, `url.py` |
| `app/chain/` | Reusable use-case orchestration across modules, services, Oper classes, events, and caches; chains reach modules only through `run_module` dispatch on method-name contracts | Transport schemas, backend-specific protocol details, generic primitives, direct imports of module internals (classes, exceptions, constants) | `media.py`, `download.py`, `subscribe.py`, `transfer.py` |
| `app/startup/` | Composition root: inject providers/adapters, order initialization and shutdown, decide restart/lifecycle policy | Reusable business rules or adapter implementation details | `lifecycle.py`, `domain_initializer.py`, `cache_initializer.py`, `managed_resources_initializer.py`, `modules_initializer.py` |
| `app/chain/` | Reusable use-case orchestration across modules, Application services, injected ports, events, and caches; chains reach modules only through `run_module` dispatch on method-name contracts | Transport schemas, backend-specific protocol details, generic primitives, direct Oper/DB imports, direct imports of module internals (classes, exceptions, constants) | `media.py`, `download.py`, `subscribe.py`, `transfer.py` |
| `app/db/oper/` | 面向表和持久化值的 SQLAlchemy 数据访问;接收调用方 Session,只查询、暂存或 flush | Application 业务规则、隐式事务所有权、外部副作用 | `subscribe.py`, `site.py`, `workflow.py` |
| `app/db/adapters/` | 实现 Application 持久化 Port,创建短生命周期 Session/UoW,并适配 Oper | 用例规则、启动顺序、进程生命周期 | `subscription.py`, `site.py`, `outbox.py`, `workflow.py` |
| `app/startup/` | Composition root: `composition/` 构造并注入跨层依赖,`initializers/` 按领域初始化,`lifecycle/` 编排启动关闭 | Reusable business rules or adapter implementation details | `composition/context.py`, `composition/database.py`, `initializers/modules.py`, `lifecycle/components.py` |
| `app/sdk/` | Deliberately curated stable imports for new plugins | Canonical implementation logic or host-internal dependencies | `browser.py`, `cache.py`, `logging.py`, `media.py`, `network.py`, `services.py` |
| `app/runtime/compat/` | 仅依赖标准库的精确旧导入路由、资源前置扫描和 DEBUG 诊断 | 业务实现、通配猜测、目标模块的提前导入 | `manifest.py`, `imports.py`, `resource_imports.py`, `diagnostics.py` |
@@ -87,9 +89,10 @@ Use these questions in order before creating or moving a module:
5. Does it perform configured network, cache, OS/process, file, package/resource, stdio, or Rust I/O? Put it under the matching `adapters` technical boundary.
6. Does it implement a named external product/ecosystem workflow? Put it in `adapters/external`.
7. Does it own authentication, authorization, signing, SSRF, URL/path safety, OTP, passkeys, or two-factor behavior? Put it in `application/security`.
8. Does it read persisted user configuration, coordinate one bounded capability, or normalize/match one service family? Put it in `application`.
9. Does it coordinate several modules/services/Oper classes for one use case? Put it in `chain`.
10. Is it public to plugins or only preserving an old path? Curate it in `sdk` or map it in `runtime/compat`; do not move implementation there.
8. Does it define a use case or the persistence Port required by that use case? Put it in `application`; do not import concrete DB there.
9. Does it implement an Application persistence Port with SQLAlchemy Session/UoW/Oper? Put it in `db/adapters`.
10. Does it coordinate several modules/services/Oper classes for one use case? Put it in `chain`.
11. Is it public to plugins or only preserving an old path? Curate it in `sdk` or map it in `runtime/compat`; do not move implementation there.
### Enforced Split Examples
@@ -97,11 +100,11 @@ These decisions are architectural constraints, not naming suggestions:
* Cache contracts, memory backends, decorators, and proxies stay in `app/runtime/cache.py`; Redis and filesystem implementations stay in `app/adapters/cache/backends.py`. Startup registers concrete factories before decorated business modules are imported. Legacy `app.core.cache` resolves to the complete `app.sdk.cache` facade.
* The complete logging runtime stays in `app/runtime/log.py`: policy, console/plugin routing, async rotating file output, and shutdown. `app.runtime.config` supplies the resolved settings and log path. `runtime/log.py` remains a dependency leaf with no `app.*` imports. Plugins use `app.sdk.logging`; legacy `app.log` resolves to that SDK facade.
* Recognition parsing stays pure in `app/domain/meta/` and `app/domain/metainfo.py`. `app/application/recognition.py` reads `SystemConfigOper`; `app/startup/domain_initializer.py` injects rules, extension policy, source defaults, TMDB image construction, and the optional Rust accelerator.
* Recognition parsing stays pure in `app/domain/meta/` and `app/domain/metainfo.py`. `app/application/recognition.py` consumes injected configuration; `app/startup/initializers/domain.py` injects rules, extension policy, source defaults, TMDB image construction, and the optional Rust accelerator.
* Kodi-style NFO reading and metadata document generation are one domain capability and stay together in `app/domain/scraper.py`; a separate `domain/nfo.py` must not be recreated.
* `app/application/mediaserver.py` is the single media-server service capability module. It owns configured service discovery together with Provider ID normalization and music-library matching, while reusing generic identity rules from `app/domain/media.py`.
* Configured notification-service discovery belongs in `app/application/notification.py`. Web Push subscription and manual-send HTTP behavior stays in `app/api/endpoints/message.py`; it is not a reusable messaging capability module.
* `app/adapters/system/resource.py` detects/downloads/installs resources and returns whether installation occurred. Only `app/startup/modules_initializer.py` may decide to restart the process afterward.
* `app/adapters/system/resource.py` detects/downloads/installs resources and returns whether installation occurred. Only `app/startup/initializers/modules.py` may decide to restart the process afterward.
* Process memory/GC policy belongs in `app/runtime/gc.py`; external IP-location APIs belong in `app/adapters/external/location.py`.
* Security implementation filenames use package-context nouns: `app/application/security/url.py` and `app/application/security/twofactor.py`. Historical `app.utils.security` and `app.helper.twofa` remain compatibility mappings only.
+1 -1
View File
@@ -17,7 +17,7 @@ from app.application.subscription.mutation import (
SubscriptionHistoryMutationRepository,
SubscriptionMutationRepository,
)
from app.startup.context import (
from app.startup.composition.context import (
AgentChatRuntime,
HostRuntime,
SubscriptionRuntime,
+1 -1
View File
@@ -17,7 +17,7 @@ from app.application.messaging.chat import (
AsyncUnitOfWork,
)
from app.application.messaging.message import MessageQueryService
from app.startup.context import AgentChatRuntime, HostRuntime
from app.startup.composition.context import AgentChatRuntime, HostRuntime
def get_agent_chat_service(
+1 -1
View File
@@ -21,7 +21,7 @@ from app.application.security.user import (
UserService,
)
from app.schemas.token import TokenPayload as _SchemaTokenPayload
from app.startup.context import HostRuntime
from app.startup.composition.context import HostRuntime
def get_user_service(
+1 -1
View File
@@ -18,7 +18,7 @@ from app.chain.storage import StorageChain
from app.runtime.events import eventmanager
from app.schemas.types import EventType
from app.schemas.workflow import FileItem as _SchemaFileItem
from app.startup.context import HostRuntime
from app.startup.composition.context import HostRuntime
def get_mediaserver_query_service(
+1 -1
View File
@@ -14,7 +14,7 @@ from app.domain import site as site_rules
from app.foundation import url as url_tools
from app.runtime.events import eventmanager
from app.schemas.types import EventType
from app.startup.context import HostRuntime
from app.startup.composition.context import HostRuntime
async def _publish_site_updated(payload: dict[str, Any]) -> None:
+1 -1
View File
@@ -37,7 +37,7 @@ from app.application.subscription.search import SearchSubscriptionsCommand
from app.runtime.events import eventmanager
from app.runtime.log import logger
from app.schemas.types import EventType
from app.startup.context import HostRuntime
from app.startup.composition.context import HostRuntime
from app.api.context import get_background_task_registry, resolve_background_task_registry
from app.runtime.tasks import TaskRegistry
+1 -1
View File
@@ -17,7 +17,7 @@ from app.application.workflow import (
)
from app.runtime.config import global_vars
from app.workflow import WorkFlowManager
from app.startup.context import HostRuntime
from app.startup.composition.context import HostRuntime
def get_workflow_mutation_command(
+2 -2
View File
@@ -13,7 +13,7 @@ from typing import Any, Callable, Optional
Provider = Callable[[], Any]
# provider 注册表由 startup/agent_initializer 在组合根装配。
# provider 注册表由 startup/initializers/agent.py 在组合根装配。
_agent_manager_provider: Optional[Provider] = None
_running_agent_manager_provider: Optional[Provider] = None
_prompt_manager_provider: Optional[Provider] = None
@@ -66,7 +66,7 @@ def _resolve(provider: Optional[Provider], service_name: str) -> Any:
if provider is None:
raise RuntimeError(
f"Agent 服务 {service_name} 未注册:"
"请先导入 app.startup.agent_initializer 完成组合根装配"
"请先导入 app.startup.initializers.agent 完成组合根装配"
)
return provider()
+1 -1
View File
@@ -11,7 +11,7 @@ Command 实现由 startup 组合根在导入期注册,避免 application 层
from typing import Any, Dict, Optional
# Command 类:由 startup/command_initializer 在导入期注册。
# Command 类:由 startup/initializers/command.py 在导入期注册。
_command_class: Any = None
+1 -1
View File
@@ -58,7 +58,7 @@ def _resolve_skill_catalog() -> SkillCatalogPort:
"""解析已注入的技能目录;缺少组合根装配时给出明确错误。"""
if _skill_catalog_provider is None:
raise RuntimeError(
"技能目录服务未注册:请先导入 app.startup.agent_initializer "
"技能目录服务未注册:请先导入 app.startup.initializers.agent "
"完成组合根装配"
)
return _skill_catalog_provider()
+1 -1
View File
@@ -17,7 +17,7 @@ from typing import Any, Awaitable, Callable, List, Optional, cast
# Agent 自主定时任务在运行时调度器中的任务 ID 前缀。
AGENT_TASK_JOB_PREFIX = "agent-task"
# Scheduler 类:由 startup/scheduler_initializer 在导入期注册。
# Scheduler 类:由 startup/initializers/scheduler.py 在导入期注册。
_scheduler_class: Any = None
+1 -1
View File
@@ -21,7 +21,7 @@ from app.runtime.settings import RuntimeSettingsCompat
settings = RuntimeSettingsCompat()
from app.runtime.state import SystemHelper
from app.application.backup import BackupArtifact
from app.startup.database import build_database_governance
from app.startup.composition.database import build_database_governance
from version import APP_VERSION
BACKEND_RUNTIME_FILE = settings.TEMP_PATH / "moviepilot.runtime.json"
+1
View File
@@ -0,0 +1 @@
"""实现 Application 持久化端口的 SQLAlchemy 适配器。"""
@@ -1,4 +1,4 @@
"""Chain durable 事件写入端口的 SQLAlchemy 启动适配器。"""
"""Chain durable 事件写入端口的 SQLAlchemy 适配器。"""
from __future__ import annotations
@@ -17,10 +17,10 @@ from app.application.chain.durable_events import (
)
from app.application.history import TransferHistoryRecord, TransferHistoryWriter
from app.application.outbox import DurableEventCommand, OutboxIntent
from app.db.adapters.outbox import SqlAlchemyOutboxRepository
from app.db.oper.downloadhistory import DownloadHistoryOper
from app.db.oper.transferhistory import TransferHistoryOper
from app.db.uow import SqlAlchemyUnitOfWork
from app.startup.outbox import SqlAlchemyOutboxRepository
class _StagingTransferHistoryWriter:
@@ -1,4 +1,4 @@
"""启动组合层使用的 SQLAlchemy outbox 持久化适配器。"""
"""Application outbox 端口的 SQLAlchemy 持久化适配器。"""
from datetime import datetime
@@ -1,9 +1,8 @@
"""订阅写入事务适配器的启动装配"""
"""订阅写入端口的 SQLAlchemy 事务适配器。"""
from collections.abc import Callable
from contextlib import AbstractAsyncContextManager, asynccontextmanager, contextmanager
from contextlib import AbstractAsyncContextManager
from datetime import datetime, timezone
from typing import Any
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Session
@@ -17,30 +16,12 @@ from app.application.subscription.write import (
subscription_added_notification_key,
subscription_added_report_key,
)
from app.application.subscription.delete import (
DeleteSubscribeCommand,
configure_delete_subscribe_scope,
)
from app.application.subscription.complete import (
CompleteSubscriptionCommand,
configure_subscription_completion_scope,
)
from app.application.subscription.mutation import (
SubscriptionMutationService,
configure_subscription_mutation_scope,
)
from app.adapters.external.server import MoviePilotServerHelper
from app.db.oper.subscribe import SubscribeOper
from app.db.oper.subscribehistory import SubscribeHistoryOper
from app.db.session import async_session_scope
from app.db.session import SessionFactory
from app.db.uow import SqlAlchemyAsyncUnitOfWork, SqlAlchemyUnitOfWork
from app.startup.outbox import (
from app.db.adapters.outbox import (
SqlAlchemyAsyncOutboxStager,
SqlAlchemyOutboxRepository,
)
from app.runtime.events import EventManager
from app.schemas.types import EventType
from app.db.oper.subscribe import SubscribeOper
from app.db.uow import SqlAlchemyAsyncUnitOfWork, SqlAlchemyUnitOfWork
class TransactionalSubscribeWriter:
@@ -146,66 +127,3 @@ class TransactionalSubscribeWriter:
delivered,
notification,
)
async def _publish_modified(payload: dict[str, Any]) -> None:
"""发布事务已提交的订阅修改事件。"""
await EventManager().async_send_event(EventType.SubscribeModified, payload)
async def _publish_deleted(payload: dict[str, Any]) -> None:
"""发布事务已提交的订阅删除事件。"""
await EventManager().async_send_event(EventType.SubscribeDeleted, payload)
def _publish_completed(payload: dict[str, Any]) -> None:
"""发布已提交的订阅完成事件。"""
EventManager().send_event(EventType.SubscribeComplete, payload)
@contextmanager
def subscription_completion_scope():
"""为同步完成链创建独占 Session、UoW 与 durable outbox。"""
session = SessionFactory()
try:
yield CompleteSubscriptionCommand(
repository=SubscribeOper(session),
unit_of_work=SqlAlchemyUnitOfWork(session),
outbox=SqlAlchemyOutboxRepository(session),
publish=_publish_completed,
)
finally:
session.close()
@asynccontextmanager
async def subscription_mutation_scope():
"""为非 HTTP 入口创建独占订阅修改会话、UoW 与 outbox。"""
async with async_session_scope() as session:
yield SubscriptionMutationService(
repository=SubscribeOper(session),
history_repository=SubscribeHistoryOper(session),
unit_of_work=SqlAlchemyAsyncUnitOfWork(session),
outbox=SqlAlchemyAsyncOutboxStager(session),
publish_modified=_publish_modified,
)
@asynccontextmanager
async def delete_subscribe_scope():
"""为非 HTTP 入口创建独占订阅删除会话、UoW 与 outbox。"""
async with async_session_scope() as session:
yield DeleteSubscribeCommand(
repository=SubscribeOper(session),
unit_of_work=SqlAlchemyAsyncUnitOfWork(session),
publish_deleted=_publish_deleted,
report_deleted=MoviePilotServerHelper.async_sub_done_durable,
outbox=SqlAlchemyAsyncOutboxStager(session),
)
def configure_transactional_subscription_scopes() -> None:
"""登记 Agent 等非 HTTP 入口复用的订阅事务作用域。"""
configure_subscription_mutation_scope(subscription_mutation_scope)
configure_delete_subscribe_scope(delete_subscribe_scope)
configure_subscription_completion_scope(subscription_completion_scope)
+36 -15
View File
@@ -1,7 +1,7 @@
"""
ORM 基类与数据访问基类。
Base 提供声明式基类与通用的行为(字典转换、增删改查便利方法);
Base 提供声明式基类与兼容行为(字典转换、增删改查便利方法);
DbOper 是各业务 Oper 的基类,持有一个可注入的会话。
"""
from collections.abc import Awaitable, Callable
@@ -12,9 +12,14 @@ from sqlalchemy import (CursorResult, Executable, Identity, Integer, Sequence,
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import DeclarativeBase, Mapped, Session, declared_attr, mapped_column
from app.runtime.config import settings
from app.db.decorators import async_db_query, async_db_update, db_query, db_update
from app.db.decorators import (
legacy_async_db_query,
legacy_async_db_update,
legacy_db_query,
legacy_db_update,
)
from app.db.uow import run_async_transaction, run_sync_transaction
from app.runtime.config import settings
T = TypeVar("T")
@@ -64,88 +69,104 @@ class Base(DeclarativeBase): # type: ignore[misc] # SQLAlchemy 无 py.typed
继承本类的模型一律使用 mapped_column() + Mapped[] 注解;确需非映射的类级属性时
用 ClassVar 显式声明,而不是把这个标志加回来。
create/get/update/delete/list/truncate 及其异步版本仅保留旧插件 ABI。宿主新代码应由
Application Command 定义事务边界,经显式 Session 调用 Oper,不得新增对这些方法的依赖。
"""
# 由 get_id_column() 在各模型中提供实际的列定义,这里只声明类型供 IDE 使用
id: Mapped[int]
@db_update
@legacy_db_update
def create(self, db: Session) -> None:
"""兼容旧插件调用:新增当前模型并提交。"""
db.add(self)
@async_db_update
@legacy_async_db_update
async def async_create(self, db: AsyncSession) -> Self:
"""兼容旧插件调用:异步新增当前模型、刷新主键并提交。"""
db.add(self)
await db.flush()
return self
@classmethod
@db_query
@legacy_db_query
def get(cls, db: Session, rid: int) -> Optional[Self]:
"""兼容旧插件调用:按主键查询当前模型。"""
return cast(
Optional[Self],
db.execute(select(cls).where(and_(cls.id == rid))).scalars().first(),
)
@classmethod
@async_db_query
@legacy_async_db_query
async def async_get(cls, db: AsyncSession, rid: int) -> Optional[Self]:
"""兼容旧插件调用:异步按主键查询当前模型。"""
result = await db.execute(select(cls).where(and_(cls.id == rid)))
return cast(Optional[Self], result.scalars().first())
@db_update
@legacy_db_update
def update(self, db: Session, payload: dict[str, Any]) -> None:
"""兼容旧插件调用:更新当前模型字段并提交。"""
for key, value in payload.items():
setattr(self, key, value)
if inspect(self).detached:
db.add(self)
@async_db_update
@legacy_async_db_update
async def async_update(
self,
db: AsyncSession,
payload: dict[str, Any],
) -> None:
"""兼容旧插件调用:异步更新当前模型字段并提交。"""
for key, value in payload.items():
setattr(self, key, value)
if inspect(self).detached:
db.add(self)
@classmethod
@db_update
@legacy_db_update
def delete(cls, db: Session, rid: Any) -> None:
"""兼容旧插件调用:按主键删除当前模型并提交。"""
db.execute(delete(cls).where(and_(cls.id == rid)))
@classmethod
@async_db_update
@legacy_async_db_update
async def async_delete(cls, db: AsyncSession, rid: Any) -> None:
"""兼容旧插件调用:异步按主键删除当前模型并提交。"""
result = await db.execute(select(cls).where(and_(cls.id == rid)))
user = result.scalars().first()
if user:
await db.delete(user)
@classmethod
@db_update
@legacy_db_update
def truncate(cls, db: Session) -> None:
"""兼容旧插件调用:清空当前模型表并提交。"""
db.execute(delete(cls))
@classmethod
@async_db_update
@legacy_async_db_update
async def async_truncate(cls, db: AsyncSession) -> None:
"""兼容旧插件调用:异步清空当前模型表并提交。"""
await db.execute(delete(cls))
@classmethod
@db_query
@legacy_db_query
def list(cls, db: Session) -> List[Self]:
"""兼容旧插件调用:查询当前模型的全部记录。"""
return list(db.execute(select(cls)).scalars().all())
@classmethod
@async_db_query
@legacy_async_db_query
async def async_list(cls, db: AsyncSession) -> List[Self]:
"""兼容旧插件调用:异步查询当前模型的全部记录。"""
result = await db.execute(select(cls))
return list(result.scalars().all())
def to_dict(self) -> dict[str, Any]:
"""把当前模型的映射列转换为字典。"""
return {c.name: getattr(self, c.name, None) for c in self.__table__.columns} # noqa
@declared_attr.directive # type: ignore[misc] # SQLAlchemy decorator 缺少类型信息
+74 -22
View File
@@ -5,8 +5,8 @@
未显式传入会话时自动创建,并在结束时归还——异步路径经 async_session_scope 收口,
连接池与配额都在那里生效。
收尾故障(rollback / close / __aexit__ 自身抛异常)一律只记日志、不上抛,四个装饰器
的处理一致。理由与代价都要写明,别当成漏写的 raise:
收尾故障(rollback / close / __aexit__ 自身抛异常)一律只记日志、不上抛,正式装饰器
和 legacy 兼容壳的处理一致。理由与代价都要写明,别当成漏写的 raise:
- 连接断开、事务已失效这类故障恰恰最容易发生在「出错之后」的收尾阶段。裸写收尾语句时
它一抛错就顶替掉原始异常,调用方看到的只剩「connection reset」,业务异常连类型都被
@@ -28,31 +28,12 @@ from app.runtime.log import logger
_R = TypeVar("_R")
# 四个装饰器会重写实参列表:未传会话时自行创建一个并塞回 db 位置。因此包装后的可调用
# 正式装饰器会重写实参列表:未传会话时自行创建一个并塞回 db 位置。因此包装后的可调用
# 对象接受的实参与被包装函数的签名并不一致——用 Callable[..., _R] 如实表达「参数由装饰器
# 接管、返回值原样透传」。否则调用方传 None 或传异步会话都会被判成类型不符,而这恰恰是
# 装饰器存在的理由(各 Oper 的 self._db 常态就是 None)。
def run_legacy_sync_query(operation: Callable[[Session], _R]) -> _R:
"""为已移除查询装饰器的旧 Model ABI 提供一次性同步会话。"""
db = ScopedSession()
try:
return operation(db)
finally:
try:
db.close()
except Exception as close_err: # noqa: BLE001 兼容查询释放失败不改变返回语义
logger.error(f"释放数据库会话失败:{close_err}")
async def run_legacy_async_query(
operation: Callable[[AsyncSession], Awaitable[_R]],
) -> _R:
"""为移除异步查询装饰器的旧 Model ABI 提供一次性异步会话。"""
async with async_session_scope() as db:
return await operation(db)
def _get_args_db(
args: tuple[Any, ...],
kwargs: dict[str, Any],
@@ -345,6 +326,77 @@ def legacy_async_db_query(
return wrapper
def legacy_db_update(func: Callable[..., _R]) -> Callable[..., _R]:
"""保留旧 Model 同步写 ABI,并维持历史自动提交语义。
该装饰器只供已经公开的 Model/Base 方法兼容仓外插件。宿主新写路径必须
通过 Application Command、显式 Session 和 UnitOfWork 完成事务收口。
"""
@wraps(func)
def wrapper(*args: Any, **kwargs: Any) -> _R:
db = _get_args_db(args, kwargs)
owns_session = db is None
if db is None:
db = ScopedSession()
args, kwargs = _inject_legacy_db(func, args, kwargs, db)
try:
result = func(*args, **kwargs)
db.commit()
return result
except Exception:
try:
db.rollback()
except Exception as rollback_err: # noqa: BLE001 回滚失败不能掩盖原始异常
logger.error(f"事务回滚失败,原始异常将原样上抛:{rollback_err}")
raise
finally:
if owns_session:
try:
db.close()
except Exception as close_err: # noqa: BLE001 释放故障不得改变旧 ABI 结果
logger.error(f"释放数据库会话失败:{close_err}")
return wrapper
def legacy_async_db_update(
func: Callable[..., Awaitable[_R]],
) -> Callable[..., Awaitable[_R]]:
"""保留旧 Model 异步写 ABI,并维持历史自动提交语义。
该装饰器只承接既有兼容面;新宿主代码不得用它创建隐式事务。
"""
@wraps(func)
async def wrapper(*args: Any, **kwargs: Any) -> _R:
db = _get_args_async_db(args, kwargs)
owns_session = db is None
scope = None
if db is None:
scope = async_session_scope()
db = await scope.__aenter__()
args, kwargs = _inject_legacy_db(func, args, kwargs, db)
try:
result = await func(*args, **kwargs)
await db.commit()
return result
except Exception:
try:
await db.rollback()
except Exception as rollback_err: # noqa: BLE001 回滚失败不能掩盖原始异常
logger.error(f"事务回滚失败,原始异常将原样上抛:{rollback_err}")
raise
finally:
if owns_session and scope is not None:
try:
await scope.__aexit__(None, None, None)
except Exception as close_err: # noqa: BLE001 释放故障不得改变旧 ABI 结果
logger.error(f"释放数据库会话失败:{close_err}")
return wrapper
def _inject_legacy_db(
func: Callable[..., _R],
args: tuple[Any, ...],
+5 -7
View File
@@ -4,7 +4,7 @@ from sqlalchemy import Boolean, Index, Integer, String, Text, select, update
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db.base import Base, execute_dml, get_id_column
from app.db.decorators import run_legacy_sync_query
from app.db.decorators import legacy_db_query
def _get_for_user_statement(
@@ -85,6 +85,7 @@ class AgentTask(Base):
return task.id
@classmethod
@legacy_db_query
def get_for_user(
cls,
db: Session | int | None = None,
@@ -105,11 +106,10 @@ class AgentTask(Base):
_get_for_user_statement(cls, task_id=task_id, user_id=user_id)
).scalars().first()
if isinstance(db, Session):
return query(db)
return run_legacy_sync_query(query)
return query(db)
@classmethod
@legacy_db_query
def list_for_user(
cls,
db: Session | None = None,
@@ -125,9 +125,7 @@ class AgentTask(Base):
_list_for_user_statement(cls, user_id=user_id, enabled=enabled)
).scalars().all())
if isinstance(db, Session):
return query(db)
return run_legacy_sync_query(query)
return query(db)
@classmethod
def update_task(
+9 -5
View File
@@ -5,7 +5,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db.base import Base, execute_dml, get_id_column
from app.db.decorators import run_legacy_async_query, run_legacy_sync_query
from app.db.decorators import legacy_async_db_query, legacy_db_query
class Message(Base):
@@ -49,6 +49,7 @@ class Message(Base):
return self.to_dict()
@classmethod
@legacy_db_query
def list_by_page(
cls,
db: Session | None = None,
@@ -67,9 +68,10 @@ class Message(Base):
.limit(count)
).scalars().all())
return query(db) if isinstance(db, Session) else run_legacy_sync_query(query)
return query(db)
@classmethod
@legacy_db_query
def exists_by_source(
cls,
db: Session | str | None = None,
@@ -93,9 +95,10 @@ class Message(Base):
select(cls.id).where(cls.source == source).limit(1)
).scalars().first() is not None
return query(db) if isinstance(db, Session) else run_legacy_sync_query(query)
return query(db)
@classmethod
@legacy_async_db_query
async def async_list_by_page(
cls, db: AsyncSession | None = None, page: int = 1, count: int = 30
) -> List["Message"]:
@@ -112,9 +115,10 @@ class Message(Base):
)
return list(result.scalars().all())
return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
return await query(db)
@classmethod
@legacy_async_db_query
async def async_list_sent_by_page(
cls,
db: AsyncSession | None = None,
@@ -155,7 +159,7 @@ class Message(Base):
)
return list(result.scalars().all())
return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
return await query(db)
@classmethod
def delete_before(
+4 -7
View File
@@ -8,7 +8,6 @@ from app.db.base import Base, get_id_column
from app.db.decorators import (
legacy_async_db_query,
legacy_db_query,
run_legacy_sync_query,
)
@@ -55,6 +54,7 @@ class PassKey(Base):
transports: Mapped[Optional[str]] = mapped_column(String, nullable=True)
@classmethod
@legacy_db_query
def get_by_user_id(
cls,
db: Session | int | None = None,
@@ -72,9 +72,7 @@ class PassKey(Base):
_get_by_user_id_statement(cls, user_id)
).scalars().all())
if isinstance(db, Session):
return query(db)
return run_legacy_sync_query(query)
return query(db)
@classmethod
@legacy_async_db_query
@@ -86,6 +84,7 @@ class PassKey(Base):
return list(result.scalars().all())
@classmethod
@legacy_db_query
def get_by_credential_id(
cls,
db: Session | str | None = None,
@@ -103,9 +102,7 @@ class PassKey(Base):
_get_by_credential_id_statement(cls, credential_id)
).scalars().first()
if isinstance(db, Session):
return query(db)
return run_legacy_sync_query(query)
return query(db)
@classmethod
@legacy_async_db_query
+7 -23
View File
@@ -4,7 +4,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db.base import get_id_column, Base
from app.db.decorators import run_legacy_async_query, run_legacy_sync_query
from app.db.decorators import legacy_async_db_query, legacy_db_query
class PluginData(Base):
@@ -21,54 +21,44 @@ class PluginData(Base):
)
@classmethod
@legacy_db_query
def get_plugin_data(cls, db: Session | None = None, plugin_id: str | None = None):
"""在调用方 Session 中读取插件全部数据,并兼容旧无会话入口。"""
if plugin_id is None:
raise TypeError("plugin_id is required")
if not isinstance(db, Session):
return run_legacy_sync_query(lambda session: cls.get_plugin_data(session, plugin_id))
return list(db.execute(select(cls).where(cls.plugin_id == plugin_id)).scalars().all())
@classmethod
@legacy_async_db_query
async def async_get_plugin_data(
cls, db: AsyncSession | None = None, plugin_id: str | None = None
):
"""在调用方 AsyncSession 中读取插件全部数据,并兼容旧无会话入口。"""
if plugin_id is None:
raise TypeError("plugin_id is required")
if not isinstance(db, AsyncSession):
return await run_legacy_async_query(
lambda session: cls.async_get_plugin_data(session, plugin_id)
)
result = await db.execute(select(cls).where(cls.plugin_id == plugin_id))
return list(result.scalars().all())
@classmethod
@legacy_db_query
def get_plugin_data_by_key(
cls, db: Session | None = None, plugin_id: str | None = None, key: str | None = None
):
"""在调用方 Session 中按键读取插件数据,并兼容旧无会话入口。"""
if plugin_id is None or key is None:
raise TypeError("plugin_id and key are required")
if not isinstance(db, Session):
return run_legacy_sync_query(
lambda session: cls.get_plugin_data_by_key(session, plugin_id, key)
)
return db.execute(
select(cls).where(cls.plugin_id == plugin_id, cls.key == key)
).scalars().first()
@classmethod
@legacy_async_db_query
async def async_get_plugin_data_by_key(
cls, db: AsyncSession | None = None, plugin_id: str | None = None, key: str | None = None
):
"""在调用方 AsyncSession 中按键读取插件数据,并兼容旧无会话入口。"""
if plugin_id is None or key is None:
raise TypeError("plugin_id and key are required")
if not isinstance(db, AsyncSession):
return await run_legacy_async_query(
lambda session: cls.async_get_plugin_data_by_key(session, plugin_id, key)
)
result = await db.execute(
select(cls).where(cls.plugin_id == plugin_id, cls.key == key)
)
@@ -85,28 +75,22 @@ class PluginData(Base):
db.execute(delete(cls).where(cls.plugin_id == plugin_id))
@classmethod
@legacy_db_query
def get_plugin_data_by_plugin_id(
cls, db: Session | None = None, plugin_id: str | None = None
):
"""在调用方 Session 中按插件 ID 读取数据,并兼容旧无会话入口。"""
if plugin_id is None:
raise TypeError("plugin_id is required")
if not isinstance(db, Session):
return run_legacy_sync_query(
lambda session: cls.get_plugin_data_by_plugin_id(session, plugin_id)
)
return list(db.execute(select(cls).where(cls.plugin_id == plugin_id)).scalars().all())
@classmethod
@legacy_async_db_query
async def async_get_plugin_data_by_plugin_id(
cls, db: AsyncSession | None = None, plugin_id: str | None = None
):
"""在调用方 AsyncSession 中按插件 ID 读取数据,并兼容旧无会话入口。"""
if plugin_id is None:
raise TypeError("plugin_id is required")
if not isinstance(db, AsyncSession):
return await run_legacy_async_query(
lambda session: cls.async_get_plugin_data_by_plugin_id(session, plugin_id)
)
result = await db.execute(select(cls).where(cls.plugin_id == plugin_id))
return list(result.scalars().all())
+17 -9
View File
@@ -6,7 +6,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db.base import Base, get_id_column
from app.db.decorators import run_legacy_async_query, run_legacy_sync_query
from app.db.decorators import legacy_async_db_query, legacy_db_query
class Site(Base):
@@ -58,6 +58,7 @@ class Site(Base):
downloader: Mapped[Optional[str]] = mapped_column(String)
@classmethod
@legacy_db_query
def get_by_domain(cls, db: Session | str | None = None, domain: str | None = None):
"""按域名查询站点,兼容显式会话和旧插件无会话调用。"""
if domain is None and isinstance(db, str):
@@ -69,9 +70,10 @@ class Site(Base):
"""在给定同步会话中执行域名查询。"""
return session.execute(select(cls).where(cls.domain == domain)).scalars().first()
return query(db) if isinstance(db, Session) else run_legacy_sync_query(query)
return query(db)
@classmethod
@legacy_async_db_query
async def async_get_by_domain(
cls,
db: AsyncSession | str | None = None,
@@ -88,9 +90,10 @@ class Site(Base):
result = await session.execute(select(cls).where(cls.domain == domain))
return result.scalar_one_or_none()
return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
return await query(db)
@classmethod
@legacy_async_db_query
async def async_get_by_name(
cls,
db: AsyncSession | str | None = None,
@@ -107,18 +110,20 @@ class Site(Base):
result = await session.execute(select(cls).where(cls.name == name))
return result.scalar_one_or_none()
return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
return await query(db)
@classmethod
@legacy_db_query
def get_actives(cls, db: Session | None = None):
"""查询启用站点,兼容显式会话和旧插件无会话调用。"""
def query(session: Session):
"""在给定同步会话中执行启用站点查询。"""
return list(session.execute(select(cls).where(cls.is_active.is_(True))).scalars().all())
return query(db) if isinstance(db, Session) else run_legacy_sync_query(query)
return query(db)
@classmethod
@legacy_async_db_query
async def async_get_actives(cls, db: AsyncSession | None = None):
"""异步查询启用站点,兼容显式会话和旧插件无会话调用。"""
async def query(session: AsyncSession):
@@ -126,18 +131,20 @@ class Site(Base):
result = await session.execute(select(cls).where(cls.is_active.is_(True)))
return list(result.scalars().all())
return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
return await query(db)
@classmethod
@legacy_db_query
def list_order_by_pri(cls, db: Session | None = None):
"""按优先级升序查询站点,兼容显式会话和旧插件无会话调用。"""
def query(session: Session):
"""在给定同步会话中执行优先级查询。"""
return list(session.execute(select(cls).order_by(cls.pri)).scalars().all())
return query(db) if isinstance(db, Session) else run_legacy_sync_query(query)
return query(db)
@classmethod
@legacy_async_db_query
async def async_list_order_by_pri(cls, db: AsyncSession | None = None):
"""异步按优先级升序查询站点,兼容显式会话和旧插件无会话调用。"""
async def query(session: AsyncSession):
@@ -145,9 +152,10 @@ class Site(Base):
result = await session.execute(select(cls).order_by(cls.pri))
return list(result.scalars().all())
return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
return await query(db)
@classmethod
@legacy_db_query
def get_domains_by_ids(
cls,
db: Session | list[int] | None = None,
@@ -165,7 +173,7 @@ class Site(Base):
"""在给定同步会话中执行域名投影查询。"""
return list(session.execute(select(cls.domain).where(cls.id.in_(ids))).scalars().all())
return query(db) if isinstance(db, Session) else run_legacy_sync_query(query)
return query(db)
@classmethod
def reset(cls, db: Session):
+3 -4
View File
@@ -4,7 +4,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db.base import Base, get_id_column
from app.db.decorators import run_legacy_async_query
from app.db.decorators import legacy_async_db_query
class SiteIcon(Base):
@@ -27,6 +27,7 @@ class SiteIcon(Base):
return db.execute(select(cls).where(cls.domain == domain)).scalars().first()
@classmethod
@legacy_async_db_query
async def async_get_by_domain(
cls,
db: AsyncSession | None = None,
@@ -41,6 +42,4 @@ class SiteIcon(Base):
result = await session.execute(select(cls).where(cls.domain == domain))
return result.scalar_one_or_none()
if isinstance(db, AsyncSession):
return await query(db)
return await run_legacy_async_query(query)
return await query(db)
+3 -4
View File
@@ -6,7 +6,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db.base import get_id_column, Base
from app.db.decorators import run_legacy_async_query
from app.db.decorators import legacy_async_db_query
class SiteStatistic(Base):
@@ -35,6 +35,7 @@ class SiteStatistic(Base):
return db.execute(select(cls).where(cls.domain == domain)).scalars().first()
@classmethod
@legacy_async_db_query
async def async_get_by_domain(
cls,
db: AsyncSession | None = None,
@@ -49,9 +50,7 @@ class SiteStatistic(Base):
result = await session.execute(select(cls).where(cls.domain == domain))
return result.scalar_one_or_none()
if isinstance(db, AsyncSession):
return await query(db)
return await run_legacy_async_query(query)
return await query(db)
@classmethod
def reset(cls, db: Session):
+35 -18
View File
@@ -6,7 +6,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db.base import get_id_column, Base
from app.db.decorators import run_legacy_async_query, run_legacy_sync_query
from app.db.decorators import legacy_async_db_query, legacy_db_query
from app.db.models._constraints import media_identity_constraint
from app.schemas.types import MUSIC_ENTITY_RECORDING, MediaSource
@@ -140,6 +140,7 @@ class Subscribe(Base):
return condition
@classmethod
@legacy_db_query
def exists(
cls, db: Session | MediaSource | None = None,
media_source: MediaSource | str | None = None,
@@ -164,9 +165,10 @@ class Subscribe(Base):
return session.execute(
statement.where(cls.episode_group == episode_group)
).scalars().first()
return query(db) if isinstance(db, Session) else run_legacy_sync_query(query)
return query(db)
@classmethod
@legacy_async_db_query
async def async_exists(
cls, db: AsyncSession | MediaSource | None = None,
media_source: MediaSource | str | None = None,
@@ -192,9 +194,10 @@ class Subscribe(Base):
statement.where(cls.episode_group == episode_group)
)
return result.scalars().first()
return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
return await query(db)
@classmethod
@legacy_db_query
def exists_by_username(
cls, db: Session | str | None = None,
username: str | MediaSource | None = None,
@@ -224,9 +227,10 @@ class Subscribe(Base):
return session.execute(
statement.where(cls.episode_group == episode_group)
).scalars().first()
return query(db) if isinstance(db, Session) else run_legacy_sync_query(query)
return query(db)
@classmethod
@legacy_async_db_query
async def async_exists_by_username(
cls, db: AsyncSession | str | None = None,
username: str | MediaSource | None = None,
@@ -256,9 +260,10 @@ class Subscribe(Base):
statement.where(cls.episode_group == episode_group)
)
return result.scalars().first()
return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
return await query(db)
@classmethod
@legacy_db_query
def get_by_state(cls, db: Session | str | None = None, state: str | None = None):
"""按状态列表查询订阅,兼容显式会话和旧插件无会话调用。"""
if not isinstance(db, Session):
@@ -269,9 +274,10 @@ class Subscribe(Base):
if state:
statement = statement.where(cls.state.in_(state.split(',')))
return list(session.execute(statement).scalars().all())
return query(db) if isinstance(db, Session) else run_legacy_sync_query(query)
return query(db)
@classmethod
@legacy_async_db_query
async def async_get_by_state(
cls, db: AsyncSession | str | None = None, state: str | None = None
):
@@ -285,9 +291,10 @@ class Subscribe(Base):
statement = statement.where(cls.state.in_(state.split(',')))
result = await session.execute(statement)
return list(result.scalars().all())
return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
return await query(db)
@classmethod
@legacy_db_query
def get_by_title(
cls, db: Session | str | None = None, title: str | None = None,
season: Optional[int] = None,
@@ -301,9 +308,10 @@ class Subscribe(Base):
if season is not None:
statement = statement.where(cls.season == season)
return session.execute(statement).scalars().first()
return query(db) if isinstance(db, Session) else run_legacy_sync_query(query)
return query(db)
@classmethod
@legacy_async_db_query
async def async_get_by_title(
cls, db: AsyncSession | str | None = None, title: str | None = None,
season: Optional[int] = None,
@@ -318,9 +326,10 @@ class Subscribe(Base):
statement = statement.where(cls.season == season)
result = await session.execute(statement)
return result.scalars().first()
return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
return await query(db)
@classmethod
@legacy_async_db_query
async def async_list_by_title(
cls, db: AsyncSession | str | None = None, title: str | None = None,
season: Optional[int] = None,
@@ -335,9 +344,10 @@ class Subscribe(Base):
statement = statement.where(cls.season == season)
result = await session.execute(statement)
return list(result.scalars().all())
return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
return await query(db)
@classmethod
@legacy_db_query
def list_by_media_identity(
cls, db: Session | MediaSource | None = None,
media_source: MediaSource | str | None = None,
@@ -357,9 +367,10 @@ class Subscribe(Base):
def query(session: Session):
"""在给定会话中执行媒体身份列表查询。"""
return list(session.execute(select(cls).where(condition)).scalars().all())
return query(db) if isinstance(db, Session) else run_legacy_sync_query(query)
return query(db)
@classmethod
@legacy_async_db_query
async def async_list_by_media_identity(
cls, db: AsyncSession | MediaSource | None = None,
media_source: MediaSource | str | None = None,
@@ -380,9 +391,10 @@ class Subscribe(Base):
"""在给定异步会话中执行媒体身份列表查询。"""
result = await session.execute(select(cls).where(condition))
return list(result.scalars().all())
return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
return await query(db)
@classmethod
@legacy_db_query
def get_by(
cls, db: Session | str | None = None,
type: str | MediaSource | None = None,
@@ -407,9 +419,10 @@ class Subscribe(Base):
def query(session: Session):
"""在给定会话中执行类型媒体查询。"""
return session.execute(statement).scalars().first()
return query(db) if isinstance(db, Session) else run_legacy_sync_query(query)
return query(db)
@classmethod
@legacy_async_db_query
async def async_get_by(
cls, db: AsyncSession | str | None = None,
type: str | MediaSource | None = None,
@@ -435,9 +448,10 @@ class Subscribe(Base):
"""在给定异步会话中执行类型媒体查询。"""
result = await session.execute(query)
return result.scalars().first()
return await execute_query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(execute_query)
return await execute_query(db)
@classmethod
@legacy_db_query
def list_by_username(cls, db: Session | str | None = None, username: str | None = None,
state: Optional[str] = None, mtype: Optional[str] = None):
"""按用户筛选订阅,兼容显式会话和旧插件无会话调用。"""
@@ -451,9 +465,10 @@ class Subscribe(Base):
if mtype:
statement = statement.where(cls.type == mtype)
return list(session.execute(statement).scalars().all())
return query(db) if isinstance(db, Session) else run_legacy_sync_query(query)
return query(db)
@classmethod
@legacy_async_db_query
async def async_list_by_username(cls, db: AsyncSession | str | None = None,
username: str | None = None, state: Optional[str] = None,
mtype: Optional[str] = None):
@@ -469,9 +484,10 @@ class Subscribe(Base):
statement = statement.where(cls.type == mtype)
result = await session.execute(statement)
return list(result.scalars().all())
return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
return await query(db)
@classmethod
@legacy_db_query
def list_by_type(cls, db: Session | str | None = None, mtype: str | None = None, days: int = 7):
"""按类型查询最近时间窗内的订阅,兼容显式会话和旧插件无会话调用。"""
if not isinstance(db, Session):
@@ -483,9 +499,10 @@ class Subscribe(Base):
cls.date >= time.strftime("%Y-%m-%d %H:%M:%S",
time.localtime(time.time() - 86400 * int(days)))
)).scalars().all())
return query(db) if isinstance(db, Session) else run_legacy_sync_query(query)
return query(db)
@classmethod
@legacy_async_db_query
async def async_list_by_type(cls, db: AsyncSession | str | None = None,
mtype: str | None = None, days: int = 7):
"""异步按类型查询最近时间窗内的订阅,兼容显式会话和旧插件无会话调用。"""
@@ -499,4 +516,4 @@ class Subscribe(Base):
time.localtime(time.time() - 86400 * int(days)))
))
return list(result.scalars().all())
return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
return await query(db)
+8 -5
View File
@@ -11,7 +11,6 @@ from app.db.base import Base, execute_dml, get_id_column
from app.db.decorators import (
legacy_async_db_query,
legacy_db_query,
run_legacy_sync_query,
)
from app.db.models._constraints import media_identity_constraint
from app.schemas.types import MUSIC_ENTITY_ALBUM, MUSIC_ENTITY_RECORDING, MediaSource, MediaType
@@ -189,6 +188,7 @@ class TransferHistory(Base):
return list(result.scalars().all())
@classmethod
@legacy_db_query
def get_by_hash(
cls,
db: Session | str | None = None,
@@ -206,9 +206,10 @@ class TransferHistory(Base):
select(cls).where(cls.download_hash == download_hash)
).scalars().first()
return query(db) if isinstance(db, Session) else run_legacy_sync_query(query)
return query(db)
@classmethod
@legacy_db_query
def get_by_src(
cls, db: Session | str | None = None, src: str | None = None,
storage: Optional[str] = None
@@ -235,9 +236,10 @@ class TransferHistory(Base):
statement.order_by(cls.id.desc())
).scalars().first()
return query(db) if isinstance(db, Session) else run_legacy_sync_query(query)
return query(db)
@classmethod
@legacy_db_query
def get_success_by_src(
cls, db: Session | str | None = None, src: str | None = None,
storage: Optional[str] = None
@@ -266,9 +268,10 @@ class TransferHistory(Base):
statement.order_by(cls.id.desc())
).scalars().first()
return query(db) if isinstance(db, Session) else run_legacy_sync_query(query)
return query(db)
@classmethod
@legacy_db_query
def get_by_dest(
cls, db: Session | str | None = None, dest: str | None = None,
storage: Optional[str] = None
@@ -295,7 +298,7 @@ class TransferHistory(Base):
statement.order_by(cls.id.desc())
).scalars().first()
return query(db) if isinstance(db, Session) else run_legacy_sync_query(query)
return query(db)
@classmethod
@legacy_db_query
+9 -12
View File
@@ -4,10 +4,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Mapped, Session, mapped_column
from app.db.base import Base, get_id_column
from app.db.decorators import (
run_legacy_async_query,
run_legacy_sync_query,
)
from app.db.decorators import legacy_async_db_query, legacy_db_query
class User(Base):
@@ -38,6 +35,7 @@ class User(Base):
settings: Mapped[Optional[Any]] = mapped_column(JSON, default=dict)
@classmethod
@legacy_db_query
def get_by_name(
cls,
db: Session | str | None = None,
@@ -53,11 +51,10 @@ class User(Base):
"""在给定会话中执行用户名查询。"""
return session.execute(select(cls).where(cls.name == name)).scalars().first()
if isinstance(db, Session):
return query(db)
return run_legacy_sync_query(query)
return query(db)
@classmethod
@legacy_async_db_query
async def async_get_by_name(
cls,
db: AsyncSession | str | None = None,
@@ -74,9 +71,10 @@ class User(Base):
result = await session.execute(select(cls).filter(cls.name == name))
return result.scalars().first()
return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
return await query(db)
@classmethod
@legacy_db_query
def get_by_id(cls, db: Session | int | None = None, user_id: int | None = None):
"""按用户 ID 查询用户,兼容显式会话和旧插件无会话调用。"""
if user_id is None and isinstance(db, int):
@@ -88,11 +86,10 @@ class User(Base):
"""在给定会话中执行用户 ID 查询。"""
return session.execute(select(cls).where(cls.id == user_id)).scalars().first()
if isinstance(db, Session):
return query(db)
return run_legacy_sync_query(query)
return query(db)
@classmethod
@legacy_async_db_query
async def async_get_by_id(
cls,
db: AsyncSession | int | None = None,
@@ -109,7 +106,7 @@ class User(Base):
result = await session.execute(select(cls).filter(cls.id == user_id))
return result.scalars().first()
return await query(db) if isinstance(db, AsyncSession) else await run_legacy_async_query(query)
return await query(db)
def delete_by_name(self, db: Session, name: str):
user = self.get_by_name(db, name)
+2 -2
View File
@@ -81,8 +81,8 @@ MODULE_ALIASES: Dict[str, ModuleAlias] = {
owner="db",
),
"app.db.init": ModuleAlias(
target="app.startup.database_initializer",
replacement="app.startup.database_initializer",
target="app.startup.initializers.database",
replacement="app.startup.initializers.database",
introduced="v3.0.0",
owner="startup",
),
+1
View File
@@ -0,0 +1 @@
"""宿主运行时对象、配置快照与跨层依赖的组合构建。"""
+91
View File
@@ -0,0 +1,91 @@
"""订阅事务作用域及提交后回调的组合装配。"""
from contextlib import asynccontextmanager, contextmanager
from typing import Any
from app.adapters.external.server import MoviePilotServerHelper
from app.application.subscription.complete import (
CompleteSubscriptionCommand,
configure_subscription_completion_scope,
)
from app.application.subscription.delete import (
DeleteSubscribeCommand,
configure_delete_subscribe_scope,
)
from app.application.subscription.mutation import (
SubscriptionMutationService,
configure_subscription_mutation_scope,
)
from app.db.adapters.outbox import (
SqlAlchemyAsyncOutboxStager,
SqlAlchemyOutboxRepository,
)
from app.db.oper.subscribe import SubscribeOper
from app.db.oper.subscribehistory import SubscribeHistoryOper
from app.db.session import SessionFactory, async_session_scope
from app.db.uow import SqlAlchemyAsyncUnitOfWork, SqlAlchemyUnitOfWork
from app.runtime.events import EventManager
from app.schemas.types import EventType
async def _publish_modified(payload: dict[str, Any]) -> None:
"""发布事务已提交的订阅修改事件。"""
await EventManager().async_send_event(EventType.SubscribeModified, payload)
async def _publish_deleted(payload: dict[str, Any]) -> None:
"""发布事务已提交的订阅删除事件。"""
await EventManager().async_send_event(EventType.SubscribeDeleted, payload)
def _publish_completed(payload: dict[str, Any]) -> None:
"""发布已提交的订阅完成事件。"""
EventManager().send_event(EventType.SubscribeComplete, payload)
@contextmanager
def subscription_completion_scope():
"""为同步完成链创建独占 Session、UoW 与 durable outbox。"""
session = SessionFactory()
try:
yield CompleteSubscriptionCommand(
repository=SubscribeOper(session),
unit_of_work=SqlAlchemyUnitOfWork(session),
outbox=SqlAlchemyOutboxRepository(session),
publish=_publish_completed,
)
finally:
session.close()
@asynccontextmanager
async def subscription_mutation_scope():
"""为非 HTTP 入口创建独占订阅修改会话、UoW 与 outbox。"""
async with async_session_scope() as session:
yield SubscriptionMutationService(
repository=SubscribeOper(session),
history_repository=SubscribeHistoryOper(session),
unit_of_work=SqlAlchemyAsyncUnitOfWork(session),
outbox=SqlAlchemyAsyncOutboxStager(session),
publish_modified=_publish_modified,
)
@asynccontextmanager
async def delete_subscribe_scope():
"""为非 HTTP 入口创建独占订阅删除会话、UoW 与 outbox。"""
async with async_session_scope() as session:
yield DeleteSubscribeCommand(
repository=SubscribeOper(session),
unit_of_work=SqlAlchemyAsyncUnitOfWork(session),
publish_deleted=_publish_deleted,
report_deleted=MoviePilotServerHelper.async_sub_done_durable,
outbox=SqlAlchemyAsyncOutboxStager(session),
)
def configure_transactional_subscription_scopes() -> None:
"""登记 Agent 等非 HTTP 入口复用的订阅事务作用域。"""
configure_subscription_mutation_scope(subscription_mutation_scope)
configure_delete_subscribe_scope(delete_subscribe_scope)
configure_subscription_completion_scope(subscription_completion_scope)
+1
View File
@@ -0,0 +1 @@
"""按领域组织的宿主初始化与关闭入口。"""
@@ -17,7 +17,7 @@ from app.db.base import Base
from app.db.engine import get_engine
from app.db.models import load_all_models
from app.runtime.log import logger
from app.startup.database import build_database_governance
from app.startup.composition.database import build_database_governance
def _build_alembic_config(engine: Engine | None = None) -> Config:
@@ -53,7 +53,7 @@ from app.application.configuration import (
configure_system_config,
configure_transfer_retry_config,
)
from app.startup.configuration import (
from app.startup.composition.configuration import (
build_api_runtime_config,
build_chain_runtime_config,
build_scheduler_runtime_config,
@@ -83,7 +83,7 @@ from app.application.security.userconfig import (
)
from app.application.history import configure_transfer_history_provider
from app.application.outbox import OutboxDispatcher, configure_outbox_dispatcher
from app.startup.outbox import SqlAlchemyAsyncOutboxStager, SqlAlchemyOutboxRepository
from app.db.adapters.outbox import SqlAlchemyAsyncOutboxStager, SqlAlchemyOutboxRepository
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.workflow import WorkflowQueryService, configure_workflow_query
@@ -129,22 +129,22 @@ from app.command import CommandChain
from app.schemas.message import Message
from app.schemas.message import MessageType
from app.schemas.types import EventType, SystemConfigKey
from app.startup.agent_initializer import init_agent
from app.startup.database import build_database_governance
from app.startup.managed_resources_initializer import (
from app.startup.initializers.agent import init_agent
from app.startup.composition.database import build_database_governance
from app.startup.initializers.managed_resources import (
init_managed_resources,
stop_managed_resources,
)
from app.startup.subscription import (
TransactionalSubscribeWriter,
from app.db.adapters.subscription import TransactionalSubscribeWriter
from app.startup.composition.subscription import (
configure_transactional_subscription_scopes,
)
from app.startup.chain_events import TransactionalChainDurableEventWriter
from app.startup.download_failure import TransactionalDownloadFailureRepository
from app.startup.site import TransactionalSiteRepository
from app.startup.workflow import TransactionalWorkflowExecutionService
from app.startup.transaction import TransactionalWriteRunner
from app.startup.context import (
from app.db.adapters.chain import TransactionalChainDurableEventWriter
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
from app.startup.composition.context import (
AgentChatRuntime,
AuthenticationRuntime,
HistoryRuntime,
+13 -13
View File
@@ -8,7 +8,7 @@ from typing import Callable
from fastapi import FastAPI
from app.startup.cache_initializer import configure_cache_dependencies
from app.startup.initializers.cache import configure_cache_dependencies
# 缓存装饰器会在业务模块导入时创建后端,必须先完成适配器装配。
configure_cache_dependencies()
# urllib3-future 覆盖 urllib3 命名空间后删除了 format_header_param,导致 telebot 崩溃,需在加载模块前打补丁
@@ -37,17 +37,17 @@ from app.runtime.tasks import TaskRegistry, configure_task_registry
from app.adapters.external.server import MoviePilotServerHelper
from app.runtime.state import SystemHelper
from app.runtime.log import logger, LoggerManager
from app.startup.command_initializer import init_command, stop_command, restart_command
from app.startup.agent_initializer import stop_agent
from app.startup.domain_initializer import configure_domain_dependencies
from app.startup.modules_initializer import (
from app.startup.initializers.command import init_command, stop_command, restart_command
from app.startup.initializers.agent import stop_agent
from app.startup.initializers.domain import configure_domain_dependencies
from app.startup.initializers.modules import (
drain_events,
init_modules,
settle_events,
stop_modules,
)
from app.startup.monitor_initializer import stop_monitor, init_monitor
from app.startup.plugins_initializer import (
from app.startup.initializers.monitor import stop_monitor, init_monitor
from app.startup.initializers.plugins import (
configure_plugin_services,
execute_task,
finalize_plugins,
@@ -57,18 +57,18 @@ from app.startup.plugins_initializer import (
stop_plugin_monitor,
sync_plugins,
)
from app.startup.routers_initializer import init_routers
from app.startup.scheduler_initializer import (
from app.startup.initializers.routers import init_routers
from app.startup.initializers.scheduler import (
stop_scheduler,
init_scheduler,
init_plugin_scheduler,
)
from app.db.engine import check_connection_budget, get_engine, get_global_async_engine
from app.startup.transfer_initializer import (
from app.startup.initializers.transfer import (
replay_pending_transfers,
stop_transfer_runtime,
)
from app.startup.workflow_initializer import init_workflow, stop_workflow
from app.startup.initializers.workflow import init_workflow, stop_workflow
from app.startup.lifecycle.components import (
LifecycleComponent,
LifecycleFailurePolicy,
@@ -236,7 +236,7 @@ async def initialize_modules_component(app: FastAPI) -> None:
try:
runtime = await init_modules()
except BaseException:
from app.startup.modules_initializer import stop_database_worker
from app.startup.initializers.modules import stop_database_worker
try:
await stop_database_worker()
@@ -276,7 +276,7 @@ def prepare_database_component(app: FastAPI) -> None:
"""完成数据库建表、迁移与 head 校验后发布数据库就绪状态。"""
# Alembic 及全部 ORM 元数据只在 lifespan 真正启动时加载,create_app/import 阶段
# 继续保持不建库、不加载迁移运行时的纯 ASGI 结构语义。
from app.startup.database_initializer import (
from app.startup.initializers.database import (
prepare_database,
verify_database_revision,
)
+3 -3
View File
@@ -180,7 +180,7 @@ def prepare_backend() -> None:
"""
isolate_config_dir()
ensure_sites_stub()
from app.startup.database_initializer import init_db
from app.startup.initializers.database import init_db
init_db()
from app.db.oper.systemconfig import SystemConfigOper
from app.db.oper.userconfig import UserConfigOper
@@ -188,10 +188,10 @@ def prepare_backend() -> None:
SystemConfigOper().load_snapshot()
UserConfigOper().load_snapshot()
# 缓存装饰器在测试模块导入时即创建后端,先装配隔离配置对应的适配器。
from app.startup.cache_initializer import configure_cache_dependencies
from app.startup.initializers.cache import configure_cache_dependencies
configure_cache_dependencies()
# 测试与生产使用同一组合入口,确保领域解析器获得隔离库和测试 settings。
from app.startup.domain_initializer import configure_domain_dependencies
from app.startup.initializers.domain import configure_domain_dependencies
configure_domain_dependencies()
+1 -1
View File
@@ -6,7 +6,7 @@ from sqlalchemy import pool
from alembic import context
from app.db import Base
from app.startup.cache_initializer import configure_cache_dependencies
from app.startup.initializers.cache import configure_cache_dependencies
# 历史 migration 会调用使用 FileCache 的业务清理链,Alembic 自身也是组合入口。
configure_cache_dependencies()
+6 -5
View File
@@ -245,12 +245,12 @@ sequenceDiagram
和 TestClient 因而共享同一 fail-fast 语义。
- **引擎预热 fail-fast**:同步/异步数据库引擎在单线程期完成首次创建,
避免调度器放出大量线程后再创建引擎导致连接锁竞争。
- **类型化请求装配**`startup/context.py` 的 frozen slots `HostRuntime` 是 lifespan 内唯一宿主
- **类型化请求装配**`startup/composition/context.py` 的 frozen slots `HostRuntime` 是 lifespan 内唯一宿主
上下文,`api/context.py``app.state` 收窄到具体领域能力。认证、消息、历史、媒体服务器、站点、
订阅、工作流和请求事务均使用命名 runtime 字段,不再通过字符串仓储键定位;API、Scheduler、Chain
`HostRuntime.configuration` 获取 frozen 配置快照。系统设置管理 API 通过
`HostRuntime.settings` 的窄服务读写可变部署设置,业务域不接触 Settings 实例;生产与测试组合根统一
复用 `startup/configuration.py` 的映射。`ApiDataPorts` 仅保留旧导入 ABI,不参与正式请求链路。
复用 `startup/composition/configuration.py` 的映射。`ApiDataPorts` 仅保留旧导入 ABI,不参与正式请求链路。
- **安全模式**`MOVIEPILOT_SAFE_MODE` 会跳过插件、定时器、监控器、命令与工作流,用于故障自救。
- **进程拓扑**:全功能 V3 强制 `API_WORKERS=1`,避免每个 worker 重复启动插件和后台控制面;安全模式可临时使用多 worker 诊断,但不是正式扩容方案。
- **健康语义**`/health/live` 只确认进程和事件循环可响应;`/health/ready` 仅在数据库
@@ -375,7 +375,8 @@ flowchart LR
不再保留主题包之外的第二个写入入口。
- 规范写入口中的 Oper 只 stage mutation,不创建独立 Session、不提交;Application Command
通过请求或任务入口注入的 UnitOfWork 统一 `commit/rollback`,事件、刷新和上报只在 commit
成功后执行。订阅新增样板由 `startup/subscription.py` 创建独占 Session
成功后执行。订阅新增 Port 位于 `application/subscription/write.py`,由
`db/adapters/subscription.py` 创建独占 Session`startup/composition/subscription.py` 只装配回调,
`application/subscription/write.py` 决定事务与 post-commit 边界,`SubscribeOper.stage_add()`
只查重、`add``flush`。旧 SDK 显式构造的无会话 Oper 暂留兼容自动短会话,不得被新代码复用。
`transaction-debt-baseline.json` 当前要求正式只读查询装饰器保持为 0;原有同步/异步写装饰器
@@ -384,7 +385,7 @@ flowchart LR
`create/update/delete/truncate` 兼容包装器;AST 门禁保证显式 Session 的提交权不会被底层抢走。
- 站点、历史、工作流、Agent 会话删除和插件数据重置已经形成同构事务切片;对应 Application
Command/Service 持有 UoWOper 的 `stage_*` 方法只修改当前会话。插件数据重置从
`startup/plugins_initializer.py` 创建独占会话,插件直接使用 `PluginDataOper` 的旧 ABI 仅作兼容。
`startup/initializers/plugins.py` 注入事务能力,插件直接使用 `PluginDataOper` 的旧 ABI 仅作兼容。
- 每次表结构变更必须新增 `database/versions/` 下的 Alembic 迁移。
- 运行期业务配置使用 `SystemConfigKey` 枚举 + `SystemConfigOper`,禁止裸字符串键;
用户级配置使用 `UserConfigOper`
@@ -489,7 +490,7 @@ Agent 采用**门面 + 惰性物化**设计,避免 `application → agent` 形
```mermaid
flowchart TB
Entry["消息渠道 / API / MCP"] --> Facade["app/application/agent.py<br/>编排门面(get_agent_manager 等)"]
Reg["app/startup/agent_initializer.py<br/>导入期注册轻量 Provider"]
Reg["app/startup/initializers/agent.py<br/>导入期注册轻量 Provider"]
Facade -.能力启用或首次使用时物化.-> RT["app/agent/runtime_loader.py<br/>能力发现与服务物化"]
RT --> ORC["app/agent/orchestrator.py<br/>会话编排"]
ORC --> Tools["app/agent/tools<br/>系统工具(经 application 门面)"]
@@ -119,7 +119,7 @@ MoviePilot V3 已经完成一轮重要基础工作:原 `app/core`、`app/helpe
| 模块 | 静态出度 | 主要原因 |
| --- | ---: | --- |
| `app.agent.tools.factory` | 99 | 一次性导入全部内置工具并维护集中注册表 |
| `app.startup.modules_initializer` | 55 | 组合根职责,这是合理高出度,但仍需声明式管理 |
| `app.startup.initializers.modules` | 55 | 组合根职责,这是合理高出度,但仍需声明式管理 |
| `app.api.endpoints.system` | 54 | 系统设置、规则测试、日志、网络测试、运行控制混合 |
| `app.api.deps` | 49 | 认证、插件配置和跨端点依赖装配集中 |
| `app.agent.orchestrator` | 48 | Agent 构建、执行、工具、记忆、审计、用量混合 |
@@ -425,7 +425,7 @@ app/chain/transfer.py # 保持 TransferChain 兼容门面
#### 已有进展
`app/startup/modules_initializer.py:211-245` 已经承担托管资源、壁纸 Provider、认证载荷、DoH、站点、事件错误通知、模块、Agent 和前端的组合工作。`app/startup/lifecycle.py` 显式规定数据库预热、路由、模块、插件、调度器、监控器、命令和工作流的顺序。这是正确方向。
`app/startup/initializers/modules.py` 承担托管资源、壁纸 Provider、认证载荷、DoH、站点、事件错误通知、模块、Agent 和前端的组合工作。`app/startup/lifecycle/` 显式规定数据库预热、路由、模块、插件、调度器、监控器、命令和工作流的顺序。这是正确方向。
#### 历史泄漏与当前收口
@@ -701,7 +701,7 @@ app/application/server/share.py # 订阅/工作流等分享用例
#### 典型证据
- `app/application/messaging/skill.py` 通过 `SkillCatalogPort` 消费技能目录,`app.startup.agent_initializer` 才导入并注入 `SkillHelper`
- `app/application/messaging/skill.py` 通过 `SkillCatalogPort` 消费技能目录,`app.startup.initializers.agent` 才导入并注入 `SkillHelper`
- `app/application/plugin/routes.py` 持有 `DynamicRouteRegistry` Protocol 和注册/移除用例;FastAPI app、`app.routes``openapi_schema``setup()` 均封装在 `app/adapters/web/plugin/routes.py`
- 多个 `modules` 直接导入 `app.application.messaging.agent``mediaserver``storage` 等;其中一部分是合理 SPI 消费,一部分表明应用能力接口和具体实现未区分。
- `SystemConfigOper()` 在大量文件中被直接构造,形成持久化配置服务定位器。
@@ -15,8 +15,8 @@
### 长期整改阶段 0:治理门禁恢复(2026-08-23
- 宿主依赖基线已审查 TaskRegistry 与有界后台 owner 接入后的语义差异:当前为 `800` 个模块、`6482` 条内部导入边,12 组重点禁止边继续全部为 `0`,唯一非平凡 SCC 仍是隔离的 TMDB 移植包。
- 启动性能探针会在隔离生命周期中真实创建并释放 TaskRegistrynormal/safe 组件数分别为 `20`/`10`,CI 只读检查使用稳定的宿主模块集合和生命周期组件顺序,不再把 Python/平台模块数量当作硬合同。
- 宿主依赖基线已审查本阶段归位后的语义差异:当前为 `805` 个模块、`6514` 条内部导入边,12 组重点禁止边继续全部为 `0`,唯一非平凡 SCC 仍是隔离的 TMDB 移植包。
- 启动性能探针会在隔离生命周期中真实创建并释放 TaskRegistrynormal/safe 组件数分别为 `23`/`11`,CI 只读检查使用稳定的宿主模块集合和生命周期组件顺序,不再把 Python/平台模块数量当作硬合同。
- 官方插件快照覆盖 `plugins.v3``plugins.v2` 以及 V3 实际会从 `package.json` 回退加载的 31 个默认实现;`app/plugins/**` 仍只是宿主运行副本,不进入扫描。
- SDK 快照以各模块显式 `__all__` 为公开合同,能够记录赋值别名;`typing``__future__` 等实现期导入不再被误冻结,既有数据库备份门面已补精确导出清单。
- async 阻塞实际债务已由 fixture 中的 10 项下降到 1 项并固化低水位;剩余项是 Scheduler Agent task 查询,后续阶段迁入异步查询边界后归零。
@@ -72,7 +72,7 @@
- 继续采用单进程控制面是正确选择,不建议现在拆成微服务;插件、调度器、工作流、事件和数据库共享进程内状态,拆分会放大部署、事务和兼容成本。
- `foundation/domain/runtime/adapters/application/chain/api/startup` 的职责方向基本成立;宿主架构基线、复杂度 ratchet、异步阻塞 ratchet 当前均通过。
- 依赖图当前为 `800` 个 Python 模块、`6479` 条内部导入边;唯一非平凡 SCC 位于隔离的 TMDB 第三方移植包内部,不应为了指标归零重写。
- 依赖图当前为 `805` 个 Python 模块、`6514` 条内部导入边;唯一非平凡 SCC 位于隔离的 TMDB 第三方移植包内部,不应为了指标归零重写。
- 当前主要风险已经从“目录和依赖失控”转移到运行时协议、后台副作用的可靠性和遗留兼容面。换言之,下一阶段重点应是**语义收口和可验证性**,而不是继续搬文件或机械拆大文件。
综合评价:架构方向可持续,生产可用性较高;可演进性仍处于中等水平。现阶段没有静态审计发现必须立即推倒重来的 P0 架构问题,但存在需要按 P1/P2 计划治理的真实债务。
@@ -82,7 +82,7 @@
1. **后台任务的统一所有权已覆盖 API 入口,但仍有更深层任务机制待分级。** `app/runtime/tasks.py` 已建立 lifespan 级 TaskRegistry,启动收尾、插件 Release 刷新、Webhook E0 广播、CookieCloud E1 手工调度、消息入口、Seerr 订阅、整理历史 AI 重做、OpenAI/Anthropic 协议流和 WebAgent 断线后执行/快照保存均不再维护端点模块级任务集合或 Starlette 回调,shutdown 会停止接收、取消并有限等待,且生命周期清单明确登记其顺序。主仓 `app/` 已无裸 FastAPI `BackgroundTasks`;当前仍有约 `50` 个更底层 `create_task`/等价任务创建点,与线程池和 APScheduler 并存,后续需逐项确认 owner、取消、等待、重试、幂等和是否 durable,关键业务副作用优先接入已有 Outbox/恢复表。
2. **动态模块契约仍以 legacy 聚合语义为主。** 当前登记 `212` 个模块方法,其中 `194` 个仍使用 `legacy` aggregation,只有 `14``first_non_empty``4``ordered_list_merge``app/runtime/extensions/module/contracts.py:422-455` 已能登记 family、输入/结果标签和基础签名诊断,但 `193` 个方法没有 required parameters,调度器 `app/runtime/extensions/module/dispatcher.py:109-260` 仍主要依赖运行时反射、返回值形状和短路规则。未知第三方方法保留 legacy fallback 是兼容要求,不应删除;宿主高频能力则应逐族补齐可执行的输入校验、结果校验、超时和错误语义。
3. **查询侧数据库兼容 ABI 已完成正式装饰器清零。** 写事务装饰器和正式 `db_query/async_db_query` 均为 `0`。站点、消息、用户、订阅、下载/整理历史、工作流、MediaServer、SiteUserData、AgentChat、AgentTaskRun、TransferPending、SystemConfig、PassKey 和 SubscribeHistory 的宿主查询已迁到显式 Session 路径;对应旧插件 Model 调用由独立 `legacy_*` 外壳保留,可同时接受显式 Session 与无 Session 的位置/关键字参数。后续重点转为减少 ORM 对象跨层流转,并保持正式装饰器零回退。
4. **组合根和全局状态仍形成复杂的隐式运行时图。** Singleton 实例、模块级 provider、`configure_*` 注册函数和兼容 Facade 同时存在;它们解决了旧 ABI 和启动顺序问题,但增加测试污染、重复装配、实例身份和初始化顺序风险。`app/startup/lifecycle/__init__.py:161-376` 已有声明式生命周期,`app/startup/modules_initializer.py:505-530` 也有分阶段关闭,但尚未做到所有进程级资源都只通过 typed HostRuntime 访问。后续应以“新代码禁止新增 Service Locator/Singleton 依赖、旧入口有命中观测”为 ratchet。
4. **组合根和全局状态仍形成复杂的隐式运行时图。** Singleton 实例、模块级 provider、`configure_*` 注册函数和兼容 Facade 同时存在;它们解决了旧 ABI 和启动顺序问题,但增加测试污染、重复装配、实例身份和初始化顺序风险。`app/startup/lifecycle/__init__.py` 已有声明式生命周期,`app/startup/initializers/modules.py` 也有分阶段关闭,但尚未做到所有进程级资源都只通过 typed HostRuntime 访问。后续应以“新代码禁止新增 Service Locator/Singleton 依赖、旧入口有命中观测”为 ratchet。
### P2:中长期可演进性债务
@@ -529,7 +529,7 @@ flowchart TB
**实施记录(2026-08-21**
- `app/startup/subscription.py` 为每次规范新增创建独占同步/异步 Session
- `app/application/subscription/write.py` 定义用例 Port`app/db/adapters/subscription.py` 为每次规范新增创建独占同步/异步 Session,`app/startup/composition/subscription.py` 只负责注入
`CreateSubscriptionCommand` / `AsyncCreateSubscriptionCommand` 持有 UoWOper 只执行
查重、`add``flush`
- `SubscribeOper.stage_add()` 的查重 SQL 已收口到 Oper,不再调用 Model 自动会话装饰器;
@@ -584,7 +584,7 @@ flowchart TB
**建议结构**
```text
app/startup/context.py # HostRuntime 及构建结果
app/startup/composition/context.py # HostRuntime 及构建结果
app/api/context.py # API 可见的最小 AppState / 读取依赖
app/api/dependencies/ # 按领域拆分依赖工厂
auth.py
@@ -612,7 +612,7 @@ app/api/dependencies/ # 按领域拆分依赖工厂
**实施记录(2026-08-21**
- `app/startup/context.py` 定义 frozen slots `HostRuntime` 与首个窄能力
- `app/startup/composition/context.py` 定义 frozen slots `HostRuntime` 与首个窄能力
`AgentChatRuntime`,仓储、Session、UoW 字段均为具体 Protocol 工厂,不是字符串字典。
- `init_modules()` 保留零参数兼容签名并返回本次 lifespan 唯一 Runtime;生命周期组件把结果挂到
`app.state.host_runtime``app/api/context.py` 只向 Depends 暴露 Agent chat 的最小能力。
@@ -707,7 +707,7 @@ app/api/dependencies/ # 按领域拆分依赖工厂
Startup 路径始终使用 HostRuntime 注入。插件 SDK 的 `app.sdk.config.settings`、动态 API 返回和事件字段未改。
- 收尾批次把 API 与 Chain 余下直接配置读取全部迁入类型化 snapshot;Scheduler 继续保持为零。
`HostRuntime` 新增可变部署设置服务,只供系统设置管理 API 使用,业务 API/Chain 只接收 frozen 字段。
snapshot 构造集中到 `app/startup/configuration.py`,生产启动与测试组合根复用同一映射,避免测试默认值
snapshot 构造集中到 `app/startup/composition/configuration.py`,生产启动与测试组合根复用同一映射,避免测试默认值
漂移。canonical `settings` 直接导入低水位从 154 降到 137,`SystemConfigOper()` 保持 14 个。
- `ApiRuntimeConfig` 已覆盖搜索来源、媒体/字幕/音频后缀、重命名格式、WebPush、CookieCloud、根目录和
版本标识;`ChainRuntimeConfig` 覆盖搜索、下载、整理、刮削、AI、代理、缓存、链接、路径和 TMDB 图片域。
@@ -1080,7 +1080,7 @@ OTel 初始化只能位于 Startup/AdapterDomain/Application 只依赖 no-op-
- 新增 Application command/port
- `app/runtime/event/`
- `app/runtime/extensions/module/contracts.py`
- `app/startup/context.py` / `app/api/context.py`
- `app/startup/composition/context.py` / `app/api/context.py`
3. 对第三方移植包、旧插件 Facade 和动态 SDK 设置精确豁免,不允许 `app.* = ignore_errors`
4. CI 先检查严格目录;每次迁移扩大 include 范围。
5. 类型错误不能用无界 `Any``cast(Any, ...)` 或全文件 ignore 消音。
@@ -1099,7 +1099,7 @@ OTel 初始化只能位于 Startup/AdapterDomain/Application 只依赖 no-op-
**扩展实施记录(2026-08-22**mypy 目标运行时更新到 Python 3.14,严格清单扩大到 20 个源文件;
新增纳管配置快照和下载失败事务适配器,仍保持零错误、无全局 ignore。
Workflow 执行状态 UoW 切片将 `app/application/workflow.py``app/startup/workflow.py` 纳入 strict 清单,
Workflow 执行状态 UoW 切片将 `app/application/workflow.py``app/db/adapters/workflow.py` 纳入 strict 清单,
治理范围扩大到 22 个源文件;事务命令、仓储 Protocol 和短会话适配器保持零错误。
异步安全与契约收口继续纳管 scheduling facade、Event error policy、Module dispatcher 和 async blocking
@@ -519,7 +519,7 @@ SYMBOL_ALIASES = {
4. Event、模块、插件和安全边界改为由 startup composition root 注入 resolver、回调和错误处理器,迁移模块不再处于强连通分量。
5. 插件稳定入口收敛到 `app.sdk`;存量插件无需同步修改,官方插件可以按正常发布节奏迁移。
6. 站点二进制和数据资源迁到 `app/application/site`Build 直接生成 `app.application.site.sites`Build CI、Resources V3 manifest、Docker 和本地 CLI 使用同一目标路径。
7. 媒体识别领域不再直接读取 DB/settings,也不导入 Rust 适配器;`startup/domain_initializer.py` 统一注入实时规则、后缀策略、TMDB 图片地址、默认媒体来源和加速器。
7. 媒体识别领域不再直接读取 DB/settings,也不导入 Rust 适配器;`startup/initializers/domain.py` 统一注入实时规则、后缀策略、TMDB 图片地址、默认媒体来源和加速器。
8. 缓存按职责拆为 `runtime/cache.py`(契约、内存实现、装饰器、代理)和 `adapters/cache/backends.py`Redis、文件 I/O);旧 `app.core.cache` 指向完整 `app.sdk.cache` 门面。
9. `application/mediaserver.py` 集中负责媒体服务器的配置化服务发现、Provider ID 规范化和音乐库匹配;通用媒体身份规则继续复用 `domain/media.py`
10. GC 归入 `runtime/gc.py`,外部 IP 归属查询归入 `adapters/external/location.py`,安全能力统一在 `app/application/security/`URL 安全策略为 `url.py`,二次认证文件为 `twofactor.py`
+35 -17
View File
@@ -72,10 +72,13 @@ to make the directory tree look symmetrical.
| `app/application/messaging/` | Message rendering/routing, interactions and the Agent-to-message bridge: `interaction.py` shared interaction contracts and view helpers; `router.py` unified interaction priority and callback dispatch; `site.py`/`subscribe.py`/`skill.py` per-command sessions, input parsing and views; `media.py` media interaction state while the business workflow stays in `MediaInteractionChain`; `plugin.py` plugin input capture and plugin button callbacks; `agent.py` agent choice state, callback protocol and WebAgent bridge; `message.py` notification rendering, templates and queue. Not a public SDK recommended for direct plugin use |
| `app/application/security/` | Authentication, authorization, cookies, passkeys, OTP/two-factor, path/URL safety, SSRF and signing policy |
Application services may use domain rules, runtime contracts, Oper classes and
adapters. Multi-domain workflows still belong in the existing `app/chain/`
package. `Chain`, `Service` and `Manager` remain class patterns; they do not
create additional top-level directory categories.
Application services may use domain rules and runtime contracts. They own the
persistence Protocol needed by a use case, but must not import `app.db`,
SQLAlchemy, Session, Oper classes or concrete adapters. `app/db/adapters/`
implements those Protocols and startup injects the implementation. Multi-domain
workflows still belong in the existing `app/chain/` package. `Chain`, `Service`
and `Manager` remain class patterns; they do not create additional top-level
directory categories.
### Runtime boundaries
@@ -94,8 +97,11 @@ create additional top-level directory categories.
| `app/runtime/compat/` | Standard-library-only exact legacy import routing, resource preflight scanning and DEBUG diagnostics |
`app/startup/` remains the established composition root and is not nested under
runtime. It injects providers and callbacks, orders initialization/shutdown and
decides restart policy. Lower-level runtime modules must not import startup.
runtime. Its root contains only `composition/`, `initializers/` and `lifecycle/`:
composition constructs and injects cross-layer dependencies, initializers expose
domain-scoped startup/shutdown hooks, and lifecycle orders those hooks and decides
restart policy. Reusable persistence implementations belong in `app/db/adapters/`,
not startup. Lower-level runtime modules must not import startup.
Startup publishes its frozen, slotted `HostRuntime` through FastAPI `app.state`.
API dependencies must narrow that object to a domain runtime (for example,
`AgentChatRuntime`) instead of adding a string key to a global service map.
@@ -105,14 +111,20 @@ API, Scheduler and Chain deployment values are exposed as frozen snapshots from
`HostRuntime.configuration`; canonical callers must not add a fresh direct
`settings` import when the required field belongs to an existing snapshot.
`app.schemas` and `app.db` are compatibility facades, not implementation
dependency hubs. Host code imports concrete schema submodules; the schema root
`app.schemas` and the `app.db` package root are compatibility facades, not
implementation dependency hubs. Host code imports concrete schema submodules; the schema root
resolves its generated export manifest lazily for plugins and legacy callers.
DB internals import `base`, `decorators`, `engine`, `session`, concrete models
and Oper modules directly. `app.db.models.load_all_models()` is the explicit
composition entry used before metadata creation or migration; importing one
model must not import every table.
`app/db/oper/` owns table-oriented SQLAlchemy access and receives a caller-owned
Session. `app/db/adapters/` is the concrete persistence-adapter layer: it may
depend on Application-owned Protocols, UoW/Session and Oper implementations.
This deliberate dependency inversion is the only `DB implementation ->
Application contract` direction; Application must remain free of DB imports.
### Adapter boundaries
| Path | Ownership |
@@ -123,6 +135,7 @@ model must not import every table.
| `app/adapters/external/` | CookieCloud, plugin market, OCR, IP-location providers and MoviePilot Server |
| `app/adapters/external/plugin/client.py` | Read-only plugin-market and local-repository client over the established `PluginHelper` implementation |
| `app/adapters/system/plugin/` | Plugin package and dependency I/O (`package.py`, `dependency.py`) |
| `app/db/adapters/` | SQLAlchemy implementations of Application-owned persistence Protocols |
Generic protocol transport belongs in `adapters/network`; a named product or
ecosystem workflow belongs in `adapters/external`. An adapter may depend on
@@ -381,7 +394,7 @@ policy. `app/db` therefore has no dependency on `app/domain`.
emits no runtime logs; upper-layer owners decide whether failures are
operationally relevant.
- `app/adapters/system/resource.py` only reports whether installation occurred;
`app/startup/modules_initializer.py` supplies the loaded site-resource
`app/startup/initializers/modules.py` supplies the loaded site-resource
versions and decides whether to restart. The adapter never imports the site
application service.
- Configured notification discovery lives in
@@ -407,13 +420,15 @@ policy. `app/db` therefore has no dependency on `app/domain`.
| Direction | Status |
|---|---|
| `entrypoint -> chain / application / Oper` | Allowed according to workflow complexity |
| `chain -> module (only via run_module dispatch) / application / Oper / canonical capability` | Allowed; direct `chain -> module` imports forbidden |
| `chain -> agent implementation` | Forbidden; chains reach Agent runtime only through `app/application/agent.py`; `app/startup/agent_initializer.py` registers lightweight providers at import time, and implementations are materialized only when the capability is enabled or first used |
| `entrypoint -> chain / application / injected persistence Port` | Allowed according to workflow complexity |
| `chain -> module (only via run_module dispatch) / application / injected Port / canonical capability` | Allowed; direct `chain -> module` and `chain -> Oper` imports forbidden |
| `chain -> agent implementation` | Forbidden; chains reach Agent runtime only through `app/application/agent.py`; `app/startup/initializers/agent.py` registers lightweight providers at import time, and implementations are materialized only when the capability is enabled or first used |
| `agent.tools -> api / scheduler / command` | Forbidden; tools use `app/application/plugin/routes.py`, `plugin/folders.py`, `scheduling.py` and `commands.py` application services |
| `api -> factory` | Forbidden; the FastAPI route adapter is injected into `app/application/plugin/routes.py` by the composition root after creation |
| `application -> domain / runtime / adapter / Oper` | Allowed |
| `module -> canonical capability / Oper` | Allowed |
| `application -> domain / runtime contract` | Allowed |
| `application -> DB / Oper / concrete adapter` | Forbidden; define a Protocol in Application and inject an implementation |
| `db.adapters -> application persistence Protocol / db.oper / UoW` | Allowed; this is dependency inversion, not an upper-layer use-case call |
| `module -> canonical capability / Application persistence Port` | Allowed; direct Oper imports are forbidden for new code |
| `module -> module / chain` | Forbidden for new code |
| `adapter -> application / runtime.extensions / sdk / compat` | Forbidden |
| `domain -> runtime / adapter / application / DB` | Forbidden |
@@ -426,11 +441,14 @@ policy. `app/db` therefore has no dependency on `app/domain`.
| Path | Purpose |
|---|---|
| `app/application/agent.py` | Agent orchestration facade (`get_agent_manager` / `get_prompt_manager` / capability queries / prompt builders); lightweight providers register through `app/startup/agent_initializer.py`, with no static `application -> agent` edge |
| `app/application/agent.py` | Agent orchestration facade (`get_agent_manager` / `get_prompt_manager` / capability queries / prompt builders); lightweight providers register through `app/startup/initializers/agent.py`, with no static `application -> agent` edge |
| `app/agent/runtime_loader.py` | Agent-specific capability discovery and canonical entrypoint/service materialization; reuses the generic Capability Runtime while keeping Agent ownership under `app/agent/` |
| `app/application/subscription/write.py` | Subscription media translation and sync/async write-port orchestration |
| `app/application/scheduling.py` | Runtime scheduler facade for Agent tools and endpoints; `Scheduler` class registered by `app/startup/scheduler_initializer.py` |
| `app/application/commands.py` | Command registry facade for Agent tools and endpoints; `Command` class registered by `app/startup/command_initializer.py` |
| `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/db/adapters/` | SQLAlchemy repository/UoW implementations for Application-owned persistence Protocols |
| `app/startup/composition/` | HostRuntime, configuration snapshots and cross-layer adapter wiring |
| `app/startup/initializers/` | Domain-scoped initialization and shutdown hooks |
| `app/chain/agent.py` | `AgentChain(ChainBase)`: the chain-layer entry for Agent sessions; Agent runtime stays in `app/agent/` |
| `app/runtime/config.py` | `ConfigModel`, `Settings` and deployment configuration |
| `app/runtime/topology.py` | Single-worker full-runtime policy and safe-mode topology validation |
+3 -1
View File
@@ -116,7 +116,9 @@ except:
- Do not introduce new third-party libraries without placing them in the correct `pyproject.toml` dependency group and updating `uv.lock`: runtime packages belong in `[project].dependencies`, test/lint/build tooling in `[dependency-groups].dev`.
- Do not use `requests` or `httpx` directly for external HTTP calls - host code uses `RequestUtils` from `app/adapters/network/http.py`; plugins use `app.sdk.network`.
- Do not issue raw SQLAlchemy queries from chains, modules, or endpoints — use the Oper classes in `app/db/oper/`.
- Do not issue raw SQLAlchemy queries or import Oper classes from chains, modules,
or endpoints. Define/consume an Application persistence Port; its concrete
implementation under `app/db/adapters/` may use Oper classes from `app/db/oper/`.
- Do not add TODO or FIXME without context. Only keep one if it is genuinely deferred and cannot be addressed in the current task.
- Do not add noisy markers like `# change starts here`, `# important`, or `# this is a fix`.
- Do not write comments that restate what the code already clearly says.
+45 -18
View File
@@ -81,33 +81,49 @@ the stub.
Oper classes accept and return persistence values. Turning a `MediaInfo` or
`MetaBase` into a row is business logic and lives in `app/application/`.
Application owns use-case commands and persistence Protocols, but does not import
`app.db`, SQLAlchemy, Session or Oper. Concrete persistence is used in
`app/db/adapters/`: adapters implement those Protocols with explicit Session,
UnitOfWork and Oper objects. `app/startup/composition/` creates and injects the
adapters; it does not retain reusable repository implementations.
### Transaction ownership ratchet
- `tests/fixtures/architecture/transaction-debt-baseline.json` records the
existing Model transaction decorators. All formal query and write decorators
are now zero and must remain zero; compatibility-only `legacy_*` shells must
not be counted as new transaction ownership.
- `legacy_db_query` / `legacy_async_db_query` are compatibility-only shells for
existing plugin-facing Model methods. Host Oper code must pass an explicit
Session through `_execute_sync_query` / `_execute_async_query`; new Model
methods must not add either legacy decorator.
- `tests/fixtures/architecture/transaction-debt-baseline.json` records formal
decorators in concrete files under `app/db/models/`. Their count is zero and
must remain zero. Compatibility-only `legacy_*` shells are tracked separately
and must never be treated as the target design.
- `legacy_db_query` / `legacy_async_db_query` preserve an existing plugin-facing
Model method whose no-Session call shape cannot be removed yet. If a Model
method has no external ABI obligation, move the query into its Oper and remove
the Model method instead of adding `legacy_*`.
- `Base.create/get/update/delete/list/truncate` and their async forms are inherited
plugin ABI, so `app/db/base.py` deliberately uses legacy query/write wrappers.
New host code must not call these convenience methods; Oper staging methods and
explicit UoW are the canonical path. Removal requires plugin-usage evidence and
a separately announced compatibility break, not a mechanical rename.
- Host Oper code must pass an explicit Session through `_execute_sync_query` /
`_execute_async_query`; new Model methods must not add any legacy decorator.
- New Model methods must not use `db_query`, `db_update`, `async_db_query`, or
`async_db_update`, create a Session, or call `commit()` / `rollback()`.
- Oper receives a caller-owned Session and may query, add, update, delete, or
flush. A composable Oper method must not create its own Session and must not
commit or roll back.
- The API, Scheduler, Agent, or another logical operation entry creates the
Session and adapts it through `app/db/uow.py`. Application command code owns
`commit()` / `rollback()`; events, scheduling refresh, reports, and other
external effects run only after a successful commit.
- API, Scheduler, Agent and Chain consume an injected Application Port; they do
not import or create a Session. The concrete `app/db/adapters/` implementation
creates the Session and adapts it through `app/db/uow.py`. Application command
code decides when the injected UoW commits or rolls back; events, scheduling
refresh, reports and other external effects run only after a successful commit.
- A synchronous Session is private to one worker thread. An AsyncSession is
private to one asyncio task/operation; neither may be stored in a process
singleton or reused by concurrent work.
- Subscription creation is the reference slice: `app/startup/subscription.py`
creates an exclusive Session, `app/application/subscription/write.py` owns the
UoW and post-commit callback, and `SubscribeOper.stage_add()` only queries,
adds, and flushes. Preserve `SubscribeOper.add()` only for legacy SDK callers;
new host code must not use that auto-commit compatibility path.
- Subscription creation is the reference slice:
`app/application/subscription/write.py` owns the command and persistence Port,
`app/db/adapters/subscription.py` creates an exclusive Session and adapts Oper/UoW,
and `app/startup/composition/subscription.py` only wires scopes and post-commit
callbacks. `SubscribeOper.stage_add()` only queries, adds and flushes. Preserve
`SubscribeOper.add()` only for legacy SDK callers; new host code must not use
that auto-commit compatibility path.
- The same rule applies to `SiteMutationCommand`, history/workflow commands,
`AgentChatService.delete()`, and `DeletePluginDataCommand`: bind the repository
and UoW to one request/operation Session. Legacy plugin-facing Oper methods may
@@ -117,7 +133,18 @@ Run `./.venv/bin/python scripts/architecture/baseline.py --check-host` after
persistence changes. A deliberate debt reduction may refresh the low-water mark
with `--write-host`; never refresh it to accept newly introduced debt.
**Standard Oper method conventions:**
**Canonical explicit-session Oper conventions:**
```python
with SessionFactory() as session:
oper = SubscribeOper(session)
subscribe = oper.get(sid=1) # Query in caller-owned Session
subscribes = oper.list() # List in caller-owned Session
oper.stage_add(Subscribe(...)) # Stage only; caller-owned UoW commits
```
The following no-Session form is legacy plugin ABI only and must not be copied
into host code:
```python
oper = SubscribeOper()
+6 -6
View File
@@ -35,12 +35,12 @@ files =
app/db/decorators.py,
app/db/base.py,
app/db/uow.py,
app/startup/context.py,
app/startup/configuration.py,
app/startup/outbox.py,
app/startup/chain_events.py,
app/startup/download_failure.py,
app/startup/workflow.py,
app/startup/composition/context.py,
app/startup/composition/configuration.py,
app/db/adapters/outbox.py,
app/db/adapters/chain.py,
app/db/adapters/download.py,
app/db/adapters/workflow.py,
app/api/context.py,
app/api/dependencies/subscription.py,
scripts/architecture/async_blocking.py
+4 -4
View File
@@ -2380,12 +2380,12 @@ def _apply_local_system_config_inner(config_payload: dict[str, Any]) -> None:
sys.path.insert(0, str(ROOT))
try:
from app.startup.database_initializer import prepare_database
from app.startup.initializers.database import prepare_database
from app.db.oper.systemconfig import SystemConfigOper
from app.schemas.types import SystemConfigKey
from app.db.session import SessionFactory, async_session_scope
from app.db.uow import configure_transaction_runners
from app.startup.transaction import TransactionalWriteRunner
from app.db.adapters.transaction import TransactionalWriteRunner
except ModuleNotFoundError as exc:
raise RuntimeError(
"当前环境尚未安装 MoviePilot 运行依赖,请先执行 moviepilot install deps 或 moviepilot setup"
@@ -2560,7 +2560,7 @@ def _sync_superuser_account_inner() -> None:
sys.path.insert(0, str(ROOT))
try:
from app.startup.database_initializer import prepare_database
from app.startup.initializers.database import prepare_database
except ModuleNotFoundError as exc:
raise RuntimeError(
"当前环境尚未安装 MoviePilot 运行依赖,请先执行 moviepilot install deps 或 moviepilot setup"
@@ -3693,7 +3693,7 @@ def run_agent_request(
sys.path.insert(0, str(ROOT))
try:
from app.startup.database_initializer import prepare_database
from app.startup.initializers.database import prepare_database
from app.agent import MoviePilotAgent
from app.runtime.config import settings
except ModuleNotFoundError as exc:
+6 -6
View File
@@ -51,7 +51,7 @@ def configure_plugin_system_services():
)
from app.runtime.config import settings
from app.runtime.settings import configure_runtime_setting_provider
from app.startup.configuration import (
from app.startup.composition.configuration import (
build_api_runtime_config,
build_chain_runtime_config,
build_scheduler_runtime_config,
@@ -153,11 +153,11 @@ def configure_plugin_system_services():
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.startup.subscription import TransactionalSubscribeWriter
from app.startup.download_failure import TransactionalDownloadFailureRepository
from app.startup.site import TransactionalSiteRepository
from app.startup.workflow import TransactionalWorkflowExecutionService
from app.startup.transaction import TransactionalWriteRunner
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 compatibility_sync_session() -> Session:
"""动态读取可被存量隔离数据库用例替换的 ScopedSession。"""
@@ -25,7 +25,7 @@
"system_config_oper_constructions": {
"calls": [
{
"file": "app/startup/modules_initializer.py",
"file": "app/startup/initializers/modules.py",
"name": "SystemConfigOper"
}
],
+402 -366
View File
@@ -13,8 +13,8 @@
"runtime_to_db": [],
"workflow_to_db": []
},
"edge_count": 6483,
"edge_sha256": "0a2f82ca189468d5e954b06a2c188c4ea5380a76c7c64039c7f6eb843cb60523",
"edge_count": 6514,
"edge_sha256": "eda9bc500158baa80dc5013eb4e409aa57cb70446e59a52b7e782cb9c58f9c34",
"edges": [
"app -> app.runtime",
"app -> app.runtime.compat",
@@ -1513,7 +1513,8 @@
"app.api.context -> app.runtime",
"app.api.context -> app.runtime.tasks",
"app.api.context -> app.startup",
"app.api.context -> app.startup.context",
"app.api.context -> app.startup.composition",
"app.api.context -> app.startup.composition.context",
"app.api.dependencies.agent -> app.api",
"app.api.dependencies.agent -> app.api.context",
"app.api.dependencies.agent -> app.application",
@@ -1521,7 +1522,8 @@
"app.api.dependencies.agent -> app.application.messaging.chat",
"app.api.dependencies.agent -> app.application.messaging.message",
"app.api.dependencies.agent -> app.startup",
"app.api.dependencies.agent -> app.startup.context",
"app.api.dependencies.agent -> app.startup.composition",
"app.api.dependencies.agent -> app.startup.composition.context",
"app.api.dependencies.auth -> app.adapters",
"app.api.dependencies.auth -> app.adapters.web",
"app.api.dependencies.auth -> app.adapters.web.security",
@@ -1536,7 +1538,8 @@
"app.api.dependencies.auth -> app.schemas",
"app.api.dependencies.auth -> app.schemas.token",
"app.api.dependencies.auth -> app.startup",
"app.api.dependencies.auth -> app.startup.context",
"app.api.dependencies.auth -> app.startup.composition",
"app.api.dependencies.auth -> app.startup.composition.context",
"app.api.dependencies.data -> app.api",
"app.api.dependencies.data -> app.api.data",
"app.api.dependencies.history -> app.api",
@@ -1554,7 +1557,8 @@
"app.api.dependencies.history -> app.schemas.types",
"app.api.dependencies.history -> app.schemas.workflow",
"app.api.dependencies.history -> app.startup",
"app.api.dependencies.history -> app.startup.context",
"app.api.dependencies.history -> app.startup.composition",
"app.api.dependencies.history -> app.startup.composition.context",
"app.api.dependencies.plugin -> app.application",
"app.api.dependencies.plugin -> app.application.commands",
"app.api.dependencies.plugin -> app.application.plugin",
@@ -1582,7 +1586,8 @@
"app.api.dependencies.site -> app.schemas",
"app.api.dependencies.site -> app.schemas.types",
"app.api.dependencies.site -> app.startup",
"app.api.dependencies.site -> app.startup.context",
"app.api.dependencies.site -> app.startup.composition",
"app.api.dependencies.site -> app.startup.composition.context",
"app.api.dependencies.subscription -> app.adapters",
"app.api.dependencies.subscription -> app.adapters.external",
"app.api.dependencies.subscription -> app.adapters.external.server",
@@ -1605,7 +1610,8 @@
"app.api.dependencies.subscription -> app.schemas",
"app.api.dependencies.subscription -> app.schemas.types",
"app.api.dependencies.subscription -> app.startup",
"app.api.dependencies.subscription -> app.startup.context",
"app.api.dependencies.subscription -> app.startup.composition",
"app.api.dependencies.subscription -> app.startup.composition.context",
"app.api.dependencies.workflow -> app.adapters",
"app.api.dependencies.workflow -> app.adapters.external",
"app.api.dependencies.workflow -> app.adapters.external.server",
@@ -1617,7 +1623,8 @@
"app.api.dependencies.workflow -> app.runtime",
"app.api.dependencies.workflow -> app.runtime.config",
"app.api.dependencies.workflow -> app.startup",
"app.api.dependencies.workflow -> app.startup.context",
"app.api.dependencies.workflow -> app.startup.composition",
"app.api.dependencies.workflow -> app.startup.composition.context",
"app.api.dependencies.workflow -> app.workflow",
"app.api.deps -> app.api",
"app.api.deps -> app.api.dependencies",
@@ -2666,6 +2673,8 @@
"app.application.notification -> app.schemas",
"app.application.notification -> app.schemas.system",
"app.application.notification -> app.schemas.types",
"app.application.plugin.config -> app.schemas",
"app.application.plugin.config -> app.schemas.exception",
"app.application.plugin.folders -> app.application",
"app.application.plugin.folders -> app.application.configuration",
"app.application.plugin.folders -> app.runtime",
@@ -3457,7 +3466,8 @@
"app.cli -> app.runtime.settings",
"app.cli -> app.runtime.state",
"app.cli -> app.startup",
"app.cli -> app.startup.database",
"app.cli -> app.startup.composition",
"app.cli -> app.startup.composition.database",
"app.command -> app.application",
"app.command -> app.application.messaging",
"app.command -> app.application.messaging.message",
@@ -3484,6 +3494,49 @@
"app.command -> app.schemas.event",
"app.command -> app.schemas.message",
"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.history",
"app.db.adapters.chain -> app.application.outbox",
"app.db.adapters.chain -> app.db",
"app.db.adapters.chain -> app.db.adapters",
"app.db.adapters.chain -> app.db.adapters.outbox",
"app.db.adapters.chain -> app.db.oper",
"app.db.adapters.chain -> app.db.oper.downloadhistory",
"app.db.adapters.chain -> app.db.oper.transferhistory",
"app.db.adapters.chain -> app.db.uow",
"app.db.adapters.download -> app.db",
"app.db.adapters.download -> app.db.oper",
"app.db.adapters.download -> app.db.oper.downloadfailure",
"app.db.adapters.download -> app.db.uow",
"app.db.adapters.outbox -> app.application",
"app.db.adapters.outbox -> app.application.outbox",
"app.db.adapters.outbox -> app.db",
"app.db.adapters.outbox -> app.db.base",
"app.db.adapters.outbox -> app.db.models",
"app.db.adapters.outbox -> app.db.models.outbox",
"app.db.adapters.site -> app.db",
"app.db.adapters.site -> app.db.oper",
"app.db.adapters.site -> app.db.oper.site",
"app.db.adapters.site -> app.db.uow",
"app.db.adapters.subscription -> app.application",
"app.db.adapters.subscription -> app.application.subscription",
"app.db.adapters.subscription -> app.application.subscription.write",
"app.db.adapters.subscription -> app.db",
"app.db.adapters.subscription -> app.db.adapters",
"app.db.adapters.subscription -> app.db.adapters.outbox",
"app.db.adapters.subscription -> app.db.oper",
"app.db.adapters.subscription -> app.db.oper.subscribe",
"app.db.adapters.subscription -> app.db.uow",
"app.db.adapters.transaction -> app.db",
"app.db.adapters.transaction -> app.db.uow",
"app.db.adapters.workflow -> app.application",
"app.db.adapters.workflow -> app.application.workflow",
"app.db.adapters.workflow -> app.db",
"app.db.adapters.workflow -> app.db.oper",
"app.db.adapters.workflow -> app.db.oper.workflow",
"app.db.adapters.workflow -> app.db.uow",
"app.db.base -> app.db",
"app.db.base -> app.db.decorators",
"app.db.base -> app.db.uow",
@@ -5587,6 +5640,8 @@
"app.runtime.extensions.module_manager -> app.runtime.settings",
"app.runtime.extensions.module_manager -> app.schemas",
"app.runtime.extensions.module_manager -> app.schemas.types",
"app.runtime.extensions.plugin.admission -> app.schemas",
"app.runtime.extensions.plugin.admission -> app.schemas.exception",
"app.runtime.extensions.plugin.catalog -> app.foundation",
"app.runtime.extensions.plugin.catalog -> app.foundation.version",
"app.runtime.extensions.plugin.catalog -> app.runtime",
@@ -5653,6 +5708,7 @@
"app.runtime.extensions.plugin_manager -> app.runtime.extensions",
"app.runtime.extensions.plugin_manager -> app.runtime.extensions.plugin",
"app.runtime.extensions.plugin_manager -> app.runtime.extensions.plugin.access",
"app.runtime.extensions.plugin_manager -> app.runtime.extensions.plugin.admission",
"app.runtime.extensions.plugin_manager -> app.runtime.extensions.plugin.catalog",
"app.runtime.extensions.plugin_manager -> app.runtime.extensions.plugin.clone",
"app.runtime.extensions.plugin_manager -> app.runtime.extensions.plugin.dependency",
@@ -5673,6 +5729,7 @@
"app.runtime.extensions.plugin_manager -> app.runtime.settings",
"app.runtime.extensions.plugin_manager -> app.runtime.thread",
"app.runtime.extensions.plugin_manager -> app.schemas",
"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.service_config -> app.runtime",
@@ -5962,109 +6019,295 @@
"app.sdk.utilities -> app.runtime.scheduling",
"app.sdk.utilities -> app.sdk",
"app.sdk.utilities -> app.sdk.string",
"app.startup.agent_initializer -> app.agent",
"app.startup.agent_initializer -> app.agent.llm",
"app.startup.agent_initializer -> app.agent.llm.capability",
"app.startup.agent_initializer -> app.agent.llm.gateway",
"app.startup.agent_initializer -> app.agent.llm.helper",
"app.startup.agent_initializer -> app.agent.llm.provider",
"app.startup.agent_initializer -> app.agent.prompt",
"app.startup.agent_initializer -> app.agent.prompt.transfer_redo",
"app.startup.agent_initializer -> app.agent.runtime_loader",
"app.startup.agent_initializer -> app.agent.skills",
"app.startup.agent_initializer -> app.agent.skills.registry",
"app.startup.agent_initializer -> app.agent.tools",
"app.startup.agent_initializer -> app.agent.tools.base",
"app.startup.agent_initializer -> app.application",
"app.startup.agent_initializer -> app.application.agent",
"app.startup.agent_initializer -> app.application.messaging",
"app.startup.agent_initializer -> app.application.messaging.skill",
"app.startup.agent_initializer -> app.runtime",
"app.startup.agent_initializer -> app.runtime.events",
"app.startup.agent_initializer -> app.runtime.log",
"app.startup.agent_initializer -> app.runtime.settings",
"app.startup.agent_initializer -> app.schemas",
"app.startup.agent_initializer -> app.schemas.types",
"app.startup.cache_initializer -> app.adapters",
"app.startup.cache_initializer -> app.adapters.cache",
"app.startup.cache_initializer -> app.adapters.cache.backends",
"app.startup.chain_events -> app.application",
"app.startup.chain_events -> app.application.chain",
"app.startup.chain_events -> app.application.chain.durable_events",
"app.startup.chain_events -> app.application.history",
"app.startup.chain_events -> app.application.outbox",
"app.startup.chain_events -> app.db",
"app.startup.chain_events -> app.db.oper",
"app.startup.chain_events -> app.db.oper.downloadhistory",
"app.startup.chain_events -> app.db.oper.transferhistory",
"app.startup.chain_events -> app.db.uow",
"app.startup.chain_events -> app.startup",
"app.startup.chain_events -> app.startup.outbox",
"app.startup.command_initializer -> app.application",
"app.startup.command_initializer -> app.application.commands",
"app.startup.command_initializer -> app.command",
"app.startup.configuration -> app.application",
"app.startup.configuration -> app.application.configuration",
"app.startup.configuration -> app.runtime",
"app.startup.configuration -> app.runtime.config",
"app.startup.configuration -> app.schemas",
"app.startup.configuration -> app.schemas.types",
"app.startup.context -> app.application",
"app.startup.context -> app.application.configuration",
"app.startup.context -> app.application.messaging",
"app.startup.context -> app.application.messaging.chat",
"app.startup.context -> app.application.outbox",
"app.startup.context -> app.application.subscription",
"app.startup.context -> app.application.subscription.delete",
"app.startup.context -> app.application.subscription.identity",
"app.startup.context -> app.application.subscription.mutation",
"app.startup.context -> app.application.workflow",
"app.startup.context -> app.runtime",
"app.startup.context -> app.runtime.tasks",
"app.startup.database -> app.adapters",
"app.startup.database -> app.adapters.system",
"app.startup.database -> app.adapters.system.backup",
"app.startup.database -> app.adapters.system.backup.database",
"app.startup.database -> app.application",
"app.startup.database -> app.application.backup",
"app.startup.database -> app.application.database",
"app.startup.database -> app.application.maintenance",
"app.startup.database -> app.db",
"app.startup.database -> app.db.engine",
"app.startup.database -> app.db.health",
"app.startup.database -> app.db.maintenance",
"app.startup.database -> app.db.session",
"app.startup.database -> app.runtime",
"app.startup.database -> app.runtime.settings",
"app.startup.database_initializer -> app.db",
"app.startup.database_initializer -> app.db.base",
"app.startup.database_initializer -> app.db.engine",
"app.startup.database_initializer -> app.db.models",
"app.startup.database_initializer -> app.runtime",
"app.startup.database_initializer -> app.runtime.log",
"app.startup.database_initializer -> app.runtime.settings",
"app.startup.database_initializer -> app.startup",
"app.startup.database_initializer -> app.startup.database",
"app.startup.domain_initializer -> app.adapters",
"app.startup.domain_initializer -> app.adapters.system",
"app.startup.domain_initializer -> app.adapters.system.rust",
"app.startup.domain_initializer -> app.application",
"app.startup.domain_initializer -> app.application.recognition",
"app.startup.domain_initializer -> app.domain",
"app.startup.domain_initializer -> app.domain.context",
"app.startup.domain_initializer -> app.domain.media",
"app.startup.domain_initializer -> app.domain.meta",
"app.startup.domain_initializer -> app.domain.meta.customization",
"app.startup.domain_initializer -> app.domain.meta.releasegroup",
"app.startup.domain_initializer -> app.domain.meta.runtime",
"app.startup.domain_initializer -> app.domain.meta.words",
"app.startup.domain_initializer -> app.domain.metainfo",
"app.startup.domain_initializer -> app.runtime",
"app.startup.domain_initializer -> app.runtime.settings",
"app.startup.download_failure -> app.db",
"app.startup.download_failure -> app.db.oper",
"app.startup.download_failure -> app.db.oper.downloadfailure",
"app.startup.download_failure -> app.db.uow",
"app.startup.composition.configuration -> app.application",
"app.startup.composition.configuration -> app.application.configuration",
"app.startup.composition.configuration -> app.runtime",
"app.startup.composition.configuration -> app.runtime.config",
"app.startup.composition.configuration -> app.schemas",
"app.startup.composition.configuration -> app.schemas.types",
"app.startup.composition.context -> app.application",
"app.startup.composition.context -> app.application.configuration",
"app.startup.composition.context -> app.application.messaging",
"app.startup.composition.context -> app.application.messaging.chat",
"app.startup.composition.context -> app.application.outbox",
"app.startup.composition.context -> app.application.subscription",
"app.startup.composition.context -> app.application.subscription.delete",
"app.startup.composition.context -> app.application.subscription.identity",
"app.startup.composition.context -> app.application.subscription.mutation",
"app.startup.composition.context -> app.application.workflow",
"app.startup.composition.context -> app.runtime",
"app.startup.composition.context -> app.runtime.tasks",
"app.startup.composition.database -> app.adapters",
"app.startup.composition.database -> app.adapters.system",
"app.startup.composition.database -> app.adapters.system.backup",
"app.startup.composition.database -> app.adapters.system.backup.database",
"app.startup.composition.database -> app.application",
"app.startup.composition.database -> app.application.backup",
"app.startup.composition.database -> app.application.database",
"app.startup.composition.database -> app.application.maintenance",
"app.startup.composition.database -> app.db",
"app.startup.composition.database -> app.db.engine",
"app.startup.composition.database -> app.db.health",
"app.startup.composition.database -> app.db.maintenance",
"app.startup.composition.database -> app.db.session",
"app.startup.composition.database -> app.runtime",
"app.startup.composition.database -> app.runtime.settings",
"app.startup.composition.subscription -> app.adapters",
"app.startup.composition.subscription -> app.adapters.external",
"app.startup.composition.subscription -> app.adapters.external.server",
"app.startup.composition.subscription -> app.application",
"app.startup.composition.subscription -> app.application.subscription",
"app.startup.composition.subscription -> app.application.subscription.complete",
"app.startup.composition.subscription -> app.application.subscription.delete",
"app.startup.composition.subscription -> app.application.subscription.mutation",
"app.startup.composition.subscription -> app.db",
"app.startup.composition.subscription -> app.db.adapters",
"app.startup.composition.subscription -> app.db.adapters.outbox",
"app.startup.composition.subscription -> app.db.oper",
"app.startup.composition.subscription -> app.db.oper.subscribe",
"app.startup.composition.subscription -> app.db.oper.subscribehistory",
"app.startup.composition.subscription -> app.db.session",
"app.startup.composition.subscription -> app.db.uow",
"app.startup.composition.subscription -> app.runtime",
"app.startup.composition.subscription -> app.runtime.events",
"app.startup.composition.subscription -> app.schemas",
"app.startup.composition.subscription -> app.schemas.types",
"app.startup.initializers.agent -> app.agent",
"app.startup.initializers.agent -> app.agent.llm",
"app.startup.initializers.agent -> app.agent.llm.capability",
"app.startup.initializers.agent -> app.agent.llm.gateway",
"app.startup.initializers.agent -> app.agent.llm.helper",
"app.startup.initializers.agent -> app.agent.llm.provider",
"app.startup.initializers.agent -> app.agent.prompt",
"app.startup.initializers.agent -> app.agent.prompt.transfer_redo",
"app.startup.initializers.agent -> app.agent.runtime_loader",
"app.startup.initializers.agent -> app.agent.skills",
"app.startup.initializers.agent -> app.agent.skills.registry",
"app.startup.initializers.agent -> app.agent.tools",
"app.startup.initializers.agent -> app.agent.tools.base",
"app.startup.initializers.agent -> app.application",
"app.startup.initializers.agent -> app.application.agent",
"app.startup.initializers.agent -> app.application.messaging",
"app.startup.initializers.agent -> app.application.messaging.skill",
"app.startup.initializers.agent -> app.runtime",
"app.startup.initializers.agent -> app.runtime.events",
"app.startup.initializers.agent -> app.runtime.log",
"app.startup.initializers.agent -> app.runtime.settings",
"app.startup.initializers.agent -> app.schemas",
"app.startup.initializers.agent -> app.schemas.types",
"app.startup.initializers.cache -> app.adapters",
"app.startup.initializers.cache -> app.adapters.cache",
"app.startup.initializers.cache -> app.adapters.cache.backends",
"app.startup.initializers.command -> app.application",
"app.startup.initializers.command -> app.application.commands",
"app.startup.initializers.command -> app.command",
"app.startup.initializers.database -> app.db",
"app.startup.initializers.database -> app.db.base",
"app.startup.initializers.database -> app.db.engine",
"app.startup.initializers.database -> app.db.models",
"app.startup.initializers.database -> app.runtime",
"app.startup.initializers.database -> app.runtime.log",
"app.startup.initializers.database -> app.runtime.settings",
"app.startup.initializers.database -> app.startup",
"app.startup.initializers.database -> app.startup.composition",
"app.startup.initializers.database -> app.startup.composition.database",
"app.startup.initializers.domain -> app.adapters",
"app.startup.initializers.domain -> app.adapters.system",
"app.startup.initializers.domain -> app.adapters.system.rust",
"app.startup.initializers.domain -> app.application",
"app.startup.initializers.domain -> app.application.recognition",
"app.startup.initializers.domain -> app.domain",
"app.startup.initializers.domain -> app.domain.context",
"app.startup.initializers.domain -> app.domain.media",
"app.startup.initializers.domain -> app.domain.meta",
"app.startup.initializers.domain -> app.domain.meta.customization",
"app.startup.initializers.domain -> app.domain.meta.releasegroup",
"app.startup.initializers.domain -> app.domain.meta.runtime",
"app.startup.initializers.domain -> app.domain.meta.words",
"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",
"app.startup.initializers.modules -> app.adapters.external",
"app.startup.initializers.modules -> app.adapters.external.server",
"app.startup.initializers.modules -> app.adapters.network",
"app.startup.initializers.modules -> app.adapters.network.browser",
"app.startup.initializers.modules -> app.adapters.network.doh",
"app.startup.initializers.modules -> app.adapters.system",
"app.startup.initializers.modules -> app.adapters.system.host",
"app.startup.initializers.modules -> app.adapters.system.resource",
"app.startup.initializers.modules -> app.adapters.web",
"app.startup.initializers.modules -> app.adapters.web.security",
"app.startup.initializers.modules -> app.adapters.web.security.access",
"app.startup.initializers.modules -> app.api",
"app.startup.initializers.modules -> app.api.data",
"app.startup.initializers.modules -> app.application",
"app.startup.initializers.modules -> app.application.agentdata",
"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.configuration",
"app.startup.initializers.modules -> app.application.database",
"app.startup.initializers.modules -> app.application.history",
"app.startup.initializers.modules -> app.application.image",
"app.startup.initializers.modules -> app.application.messaging",
"app.startup.initializers.modules -> app.application.messaging.agent",
"app.startup.initializers.modules -> app.application.messaging.chat",
"app.startup.initializers.modules -> app.application.messaging.message",
"app.startup.initializers.modules -> app.application.module",
"app.startup.initializers.modules -> app.application.outbox",
"app.startup.initializers.modules -> app.application.plugin",
"app.startup.initializers.modules -> app.application.plugin.runtime",
"app.startup.initializers.modules -> app.application.security",
"app.startup.initializers.modules -> app.application.security.auth",
"app.startup.initializers.modules -> app.application.security.passkeys",
"app.startup.initializers.modules -> app.application.security.user",
"app.startup.initializers.modules -> app.application.security.userconfig",
"app.startup.initializers.modules -> app.application.server",
"app.startup.initializers.modules -> app.application.server.report",
"app.startup.initializers.modules -> app.application.server.share",
"app.startup.initializers.modules -> app.application.service",
"app.startup.initializers.modules -> app.application.site",
"app.startup.initializers.modules -> app.application.site.health",
"app.startup.initializers.modules -> app.application.site.query",
"app.startup.initializers.modules -> app.application.subscription",
"app.startup.initializers.modules -> app.application.subscription.write",
"app.startup.initializers.modules -> app.application.workflow",
"app.startup.initializers.modules -> app.chain",
"app.startup.initializers.modules -> app.chain.download",
"app.startup.initializers.modules -> app.chain.mediaserver",
"app.startup.initializers.modules -> app.chain.scraping",
"app.startup.initializers.modules -> app.chain.search",
"app.startup.initializers.modules -> app.chain.site",
"app.startup.initializers.modules -> app.chain.subscribe",
"app.startup.initializers.modules -> app.chain.tmdb",
"app.startup.initializers.modules -> app.chain.workflow",
"app.startup.initializers.modules -> app.command",
"app.startup.initializers.modules -> app.db",
"app.startup.initializers.modules -> app.db.adapters",
"app.startup.initializers.modules -> app.db.adapters.chain",
"app.startup.initializers.modules -> app.db.adapters.download",
"app.startup.initializers.modules -> app.db.adapters.outbox",
"app.startup.initializers.modules -> app.db.adapters.site",
"app.startup.initializers.modules -> app.db.adapters.subscription",
"app.startup.initializers.modules -> app.db.adapters.transaction",
"app.startup.initializers.modules -> app.db.adapters.workflow",
"app.startup.initializers.modules -> app.db.oper",
"app.startup.initializers.modules -> app.db.oper.agentchat",
"app.startup.initializers.modules -> app.db.oper.agenttask",
"app.startup.initializers.modules -> app.db.oper.downloadhistory",
"app.startup.initializers.modules -> app.db.oper.mediaserver",
"app.startup.initializers.modules -> app.db.oper.message",
"app.startup.initializers.modules -> app.db.oper.passkey",
"app.startup.initializers.modules -> app.db.oper.plugindata",
"app.startup.initializers.modules -> app.db.oper.site",
"app.startup.initializers.modules -> app.db.oper.subscribe",
"app.startup.initializers.modules -> app.db.oper.subscribehistory",
"app.startup.initializers.modules -> app.db.oper.systemconfig",
"app.startup.initializers.modules -> app.db.oper.transferhistory",
"app.startup.initializers.modules -> app.db.oper.transferpending",
"app.startup.initializers.modules -> app.db.oper.user",
"app.startup.initializers.modules -> app.db.oper.userconfig",
"app.startup.initializers.modules -> app.db.oper.workflow",
"app.startup.initializers.modules -> app.db.session",
"app.startup.initializers.modules -> app.db.uow",
"app.startup.initializers.modules -> app.db.worker",
"app.startup.initializers.modules -> app.runtime",
"app.startup.initializers.modules -> app.runtime.cache",
"app.startup.initializers.modules -> app.runtime.config",
"app.startup.initializers.modules -> app.runtime.events",
"app.startup.initializers.modules -> app.runtime.extensions",
"app.startup.initializers.modules -> app.runtime.extensions.module",
"app.startup.initializers.modules -> app.runtime.extensions.module.dispatcher",
"app.startup.initializers.modules -> app.runtime.extensions.module_manager",
"app.startup.initializers.modules -> app.runtime.extensions.plugin_manager",
"app.startup.initializers.modules -> app.runtime.extensions.service_config",
"app.startup.initializers.modules -> app.runtime.log",
"app.startup.initializers.modules -> app.runtime.observability",
"app.startup.initializers.modules -> app.runtime.settings",
"app.startup.initializers.modules -> app.runtime.state",
"app.startup.initializers.modules -> app.runtime.tasks",
"app.startup.initializers.modules -> app.runtime.thread",
"app.startup.initializers.modules -> app.scheduler",
"app.startup.initializers.modules -> app.schemas",
"app.startup.initializers.modules -> app.schemas.message",
"app.startup.initializers.modules -> app.schemas.types",
"app.startup.initializers.modules -> app.startup",
"app.startup.initializers.modules -> app.startup.composition",
"app.startup.initializers.modules -> app.startup.composition.configuration",
"app.startup.initializers.modules -> app.startup.composition.context",
"app.startup.initializers.modules -> app.startup.composition.database",
"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.monitor -> app.monitor",
"app.startup.initializers.monitor -> app.runtime",
"app.startup.initializers.monitor -> app.runtime.execution",
"app.startup.initializers.plugins -> app.adapters",
"app.startup.initializers.plugins -> app.adapters.external",
"app.startup.initializers.plugins -> app.adapters.external.market",
"app.startup.initializers.plugins -> app.adapters.external.plugin",
"app.startup.initializers.plugins -> app.adapters.external.plugin.client",
"app.startup.initializers.plugins -> app.adapters.external.server",
"app.startup.initializers.plugins -> app.adapters.system",
"app.startup.initializers.plugins -> app.adapters.system.host",
"app.startup.initializers.plugins -> app.adapters.system.plugin",
"app.startup.initializers.plugins -> app.adapters.system.plugin.dependency",
"app.startup.initializers.plugins -> app.adapters.system.plugin.manifest",
"app.startup.initializers.plugins -> app.adapters.system.plugin.package",
"app.startup.initializers.plugins -> app.application",
"app.startup.initializers.plugins -> app.application.configuration",
"app.startup.initializers.plugins -> app.application.plugin",
"app.startup.initializers.plugins -> app.application.plugin.catalog",
"app.startup.initializers.plugins -> app.application.plugin.data",
"app.startup.initializers.plugins -> app.application.plugin.routes",
"app.startup.initializers.plugins -> app.application.site",
"app.startup.initializers.plugins -> app.db",
"app.startup.initializers.plugins -> app.db.oper",
"app.startup.initializers.plugins -> app.db.oper.plugindata",
"app.startup.initializers.plugins -> app.db.session",
"app.startup.initializers.plugins -> app.db.uow",
"app.startup.initializers.plugins -> app.foundation",
"app.startup.initializers.plugins -> app.foundation.version",
"app.startup.initializers.plugins -> app.runtime",
"app.startup.initializers.plugins -> app.runtime.compat",
"app.startup.initializers.plugins -> app.runtime.compat.diagnostics",
"app.startup.initializers.plugins -> app.runtime.compat.resource_imports",
"app.startup.initializers.plugins -> app.runtime.config",
"app.startup.initializers.plugins -> app.runtime.execution",
"app.startup.initializers.plugins -> app.runtime.extensions",
"app.startup.initializers.plugins -> app.runtime.extensions.plugin",
"app.startup.initializers.plugins -> app.runtime.extensions.plugin.dependency",
"app.startup.initializers.plugins -> app.runtime.extensions.plugin.storage",
"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.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.routers -> app.api",
"app.startup.initializers.routers -> app.api.router_specs",
"app.startup.initializers.routers -> app.api.servarr",
"app.startup.initializers.routers -> app.api.servcookie",
"app.startup.initializers.scheduler -> app.application",
"app.startup.initializers.scheduler -> app.application.scheduling",
"app.startup.initializers.scheduler -> app.scheduler",
"app.startup.initializers.transfer -> app.chain",
"app.startup.initializers.transfer -> app.chain.transfer",
"app.startup.initializers.workflow -> app.workflow",
"app.startup.lifecycle -> app.adapters",
"app.startup.lifecycle -> app.adapters.external",
"app.startup.lifecycle -> app.adapters.external.server",
@@ -6087,233 +6330,20 @@
"app.startup.lifecycle -> app.runtime.tasks",
"app.startup.lifecycle -> app.runtime.topology",
"app.startup.lifecycle -> app.startup",
"app.startup.lifecycle -> app.startup.agent_initializer",
"app.startup.lifecycle -> app.startup.cache_initializer",
"app.startup.lifecycle -> app.startup.command_initializer",
"app.startup.lifecycle -> app.startup.database_initializer",
"app.startup.lifecycle -> app.startup.domain_initializer",
"app.startup.lifecycle -> app.startup.initializers",
"app.startup.lifecycle -> app.startup.initializers.agent",
"app.startup.lifecycle -> app.startup.initializers.cache",
"app.startup.lifecycle -> app.startup.initializers.command",
"app.startup.lifecycle -> app.startup.initializers.database",
"app.startup.lifecycle -> app.startup.initializers.domain",
"app.startup.lifecycle -> app.startup.initializers.modules",
"app.startup.lifecycle -> app.startup.initializers.monitor",
"app.startup.lifecycle -> app.startup.initializers.plugins",
"app.startup.lifecycle -> app.startup.initializers.routers",
"app.startup.lifecycle -> app.startup.initializers.scheduler",
"app.startup.lifecycle -> app.startup.initializers.transfer",
"app.startup.lifecycle -> app.startup.initializers.workflow",
"app.startup.lifecycle -> app.startup.lifecycle.components",
"app.startup.lifecycle -> app.startup.modules_initializer",
"app.startup.lifecycle -> app.startup.monitor_initializer",
"app.startup.lifecycle -> app.startup.plugins_initializer",
"app.startup.lifecycle -> app.startup.routers_initializer",
"app.startup.lifecycle -> app.startup.scheduler_initializer",
"app.startup.lifecycle -> app.startup.transfer_initializer",
"app.startup.lifecycle -> app.startup.workflow_initializer",
"app.startup.managed_resources_initializer -> app.runtime",
"app.startup.managed_resources_initializer -> app.runtime.capabilities",
"app.startup.managed_resources_initializer -> app.runtime.capabilities.runtime",
"app.startup.managed_resources_initializer -> app.runtime.extensions",
"app.startup.managed_resources_initializer -> app.runtime.extensions.managed_resource_adapter",
"app.startup.managed_resources_initializer -> app.runtime.managed_resources",
"app.startup.modules_initializer -> app.adapters",
"app.startup.modules_initializer -> app.adapters.cache",
"app.startup.modules_initializer -> app.adapters.cache.redis",
"app.startup.modules_initializer -> app.adapters.external",
"app.startup.modules_initializer -> app.adapters.external.server",
"app.startup.modules_initializer -> app.adapters.network",
"app.startup.modules_initializer -> app.adapters.network.browser",
"app.startup.modules_initializer -> app.adapters.network.doh",
"app.startup.modules_initializer -> app.adapters.system",
"app.startup.modules_initializer -> app.adapters.system.host",
"app.startup.modules_initializer -> app.adapters.system.resource",
"app.startup.modules_initializer -> app.adapters.web",
"app.startup.modules_initializer -> app.adapters.web.security",
"app.startup.modules_initializer -> app.adapters.web.security.access",
"app.startup.modules_initializer -> app.api",
"app.startup.modules_initializer -> app.api.data",
"app.startup.modules_initializer -> app.application",
"app.startup.modules_initializer -> app.application.agentdata",
"app.startup.modules_initializer -> app.application.chain",
"app.startup.modules_initializer -> app.application.chain.context",
"app.startup.modules_initializer -> app.application.chain.data",
"app.startup.modules_initializer -> app.application.chain.durable_events",
"app.startup.modules_initializer -> app.application.configuration",
"app.startup.modules_initializer -> app.application.database",
"app.startup.modules_initializer -> app.application.history",
"app.startup.modules_initializer -> app.application.image",
"app.startup.modules_initializer -> app.application.messaging",
"app.startup.modules_initializer -> app.application.messaging.agent",
"app.startup.modules_initializer -> app.application.messaging.chat",
"app.startup.modules_initializer -> app.application.messaging.message",
"app.startup.modules_initializer -> app.application.module",
"app.startup.modules_initializer -> app.application.outbox",
"app.startup.modules_initializer -> app.application.plugin",
"app.startup.modules_initializer -> app.application.plugin.runtime",
"app.startup.modules_initializer -> app.application.security",
"app.startup.modules_initializer -> app.application.security.auth",
"app.startup.modules_initializer -> app.application.security.passkeys",
"app.startup.modules_initializer -> app.application.security.user",
"app.startup.modules_initializer -> app.application.security.userconfig",
"app.startup.modules_initializer -> app.application.server",
"app.startup.modules_initializer -> app.application.server.report",
"app.startup.modules_initializer -> app.application.server.share",
"app.startup.modules_initializer -> app.application.service",
"app.startup.modules_initializer -> app.application.site",
"app.startup.modules_initializer -> app.application.site.health",
"app.startup.modules_initializer -> app.application.site.query",
"app.startup.modules_initializer -> app.application.subscription",
"app.startup.modules_initializer -> app.application.subscription.write",
"app.startup.modules_initializer -> app.application.workflow",
"app.startup.modules_initializer -> app.chain",
"app.startup.modules_initializer -> app.chain.download",
"app.startup.modules_initializer -> app.chain.mediaserver",
"app.startup.modules_initializer -> app.chain.scraping",
"app.startup.modules_initializer -> app.chain.search",
"app.startup.modules_initializer -> app.chain.site",
"app.startup.modules_initializer -> app.chain.subscribe",
"app.startup.modules_initializer -> app.chain.tmdb",
"app.startup.modules_initializer -> app.chain.workflow",
"app.startup.modules_initializer -> app.command",
"app.startup.modules_initializer -> app.db",
"app.startup.modules_initializer -> app.db.oper",
"app.startup.modules_initializer -> app.db.oper.agentchat",
"app.startup.modules_initializer -> app.db.oper.agenttask",
"app.startup.modules_initializer -> app.db.oper.downloadhistory",
"app.startup.modules_initializer -> app.db.oper.mediaserver",
"app.startup.modules_initializer -> app.db.oper.message",
"app.startup.modules_initializer -> app.db.oper.passkey",
"app.startup.modules_initializer -> app.db.oper.plugindata",
"app.startup.modules_initializer -> app.db.oper.site",
"app.startup.modules_initializer -> app.db.oper.subscribe",
"app.startup.modules_initializer -> app.db.oper.subscribehistory",
"app.startup.modules_initializer -> app.db.oper.systemconfig",
"app.startup.modules_initializer -> app.db.oper.transferhistory",
"app.startup.modules_initializer -> app.db.oper.transferpending",
"app.startup.modules_initializer -> app.db.oper.user",
"app.startup.modules_initializer -> app.db.oper.userconfig",
"app.startup.modules_initializer -> app.db.oper.workflow",
"app.startup.modules_initializer -> app.db.session",
"app.startup.modules_initializer -> app.db.uow",
"app.startup.modules_initializer -> app.db.worker",
"app.startup.modules_initializer -> app.runtime",
"app.startup.modules_initializer -> app.runtime.cache",
"app.startup.modules_initializer -> app.runtime.config",
"app.startup.modules_initializer -> app.runtime.events",
"app.startup.modules_initializer -> app.runtime.extensions",
"app.startup.modules_initializer -> app.runtime.extensions.module",
"app.startup.modules_initializer -> app.runtime.extensions.module.dispatcher",
"app.startup.modules_initializer -> app.runtime.extensions.module_manager",
"app.startup.modules_initializer -> app.runtime.extensions.plugin_manager",
"app.startup.modules_initializer -> app.runtime.extensions.service_config",
"app.startup.modules_initializer -> app.runtime.log",
"app.startup.modules_initializer -> app.runtime.observability",
"app.startup.modules_initializer -> app.runtime.settings",
"app.startup.modules_initializer -> app.runtime.state",
"app.startup.modules_initializer -> app.runtime.tasks",
"app.startup.modules_initializer -> app.runtime.thread",
"app.startup.modules_initializer -> app.scheduler",
"app.startup.modules_initializer -> app.schemas",
"app.startup.modules_initializer -> app.schemas.message",
"app.startup.modules_initializer -> app.schemas.types",
"app.startup.modules_initializer -> app.startup",
"app.startup.modules_initializer -> app.startup.agent_initializer",
"app.startup.modules_initializer -> app.startup.chain_events",
"app.startup.modules_initializer -> app.startup.configuration",
"app.startup.modules_initializer -> app.startup.context",
"app.startup.modules_initializer -> app.startup.database",
"app.startup.modules_initializer -> app.startup.download_failure",
"app.startup.modules_initializer -> app.startup.managed_resources_initializer",
"app.startup.modules_initializer -> app.startup.outbox",
"app.startup.modules_initializer -> app.startup.site",
"app.startup.modules_initializer -> app.startup.subscription",
"app.startup.modules_initializer -> app.startup.transaction",
"app.startup.modules_initializer -> app.startup.workflow",
"app.startup.monitor_initializer -> app.monitor",
"app.startup.monitor_initializer -> app.runtime",
"app.startup.monitor_initializer -> app.runtime.execution",
"app.startup.outbox -> app.application",
"app.startup.outbox -> app.application.outbox",
"app.startup.outbox -> app.db",
"app.startup.outbox -> app.db.base",
"app.startup.outbox -> app.db.models",
"app.startup.outbox -> app.db.models.outbox",
"app.startup.plugins_initializer -> app.adapters",
"app.startup.plugins_initializer -> app.adapters.external",
"app.startup.plugins_initializer -> app.adapters.external.market",
"app.startup.plugins_initializer -> app.adapters.external.plugin",
"app.startup.plugins_initializer -> app.adapters.external.plugin.client",
"app.startup.plugins_initializer -> app.adapters.external.server",
"app.startup.plugins_initializer -> app.adapters.system",
"app.startup.plugins_initializer -> app.adapters.system.host",
"app.startup.plugins_initializer -> app.adapters.system.plugin",
"app.startup.plugins_initializer -> app.adapters.system.plugin.dependency",
"app.startup.plugins_initializer -> app.adapters.system.plugin.manifest",
"app.startup.plugins_initializer -> app.adapters.system.plugin.package",
"app.startup.plugins_initializer -> app.application",
"app.startup.plugins_initializer -> app.application.configuration",
"app.startup.plugins_initializer -> app.application.plugin",
"app.startup.plugins_initializer -> app.application.plugin.catalog",
"app.startup.plugins_initializer -> app.application.plugin.data",
"app.startup.plugins_initializer -> app.application.plugin.routes",
"app.startup.plugins_initializer -> app.application.site",
"app.startup.plugins_initializer -> app.db",
"app.startup.plugins_initializer -> app.db.oper",
"app.startup.plugins_initializer -> app.db.oper.plugindata",
"app.startup.plugins_initializer -> app.db.session",
"app.startup.plugins_initializer -> app.db.uow",
"app.startup.plugins_initializer -> app.foundation",
"app.startup.plugins_initializer -> app.foundation.version",
"app.startup.plugins_initializer -> app.runtime",
"app.startup.plugins_initializer -> app.runtime.compat",
"app.startup.plugins_initializer -> app.runtime.compat.diagnostics",
"app.startup.plugins_initializer -> app.runtime.compat.resource_imports",
"app.startup.plugins_initializer -> app.runtime.config",
"app.startup.plugins_initializer -> app.runtime.extensions",
"app.startup.plugins_initializer -> app.runtime.extensions.plugin",
"app.startup.plugins_initializer -> app.runtime.extensions.plugin.dependency",
"app.startup.plugins_initializer -> app.runtime.extensions.plugin.storage",
"app.startup.plugins_initializer -> app.runtime.extensions.plugin.system",
"app.startup.plugins_initializer -> app.runtime.extensions.plugin_manager",
"app.startup.plugins_initializer -> app.runtime.log",
"app.startup.plugins_initializer -> app.runtime.managed_resources",
"app.startup.plugins_initializer -> app.runtime.settings",
"app.startup.plugins_initializer -> app.schemas",
"app.startup.plugins_initializer -> app.schemas.plugin",
"app.startup.plugins_initializer -> app.schemas.types",
"app.startup.routers_initializer -> app.api",
"app.startup.routers_initializer -> app.api.router_specs",
"app.startup.routers_initializer -> app.api.servarr",
"app.startup.routers_initializer -> app.api.servcookie",
"app.startup.scheduler_initializer -> app.application",
"app.startup.scheduler_initializer -> app.application.scheduling",
"app.startup.scheduler_initializer -> app.scheduler",
"app.startup.site -> app.db",
"app.startup.site -> app.db.oper",
"app.startup.site -> app.db.oper.site",
"app.startup.site -> app.db.uow",
"app.startup.subscription -> app.adapters",
"app.startup.subscription -> app.adapters.external",
"app.startup.subscription -> app.adapters.external.server",
"app.startup.subscription -> app.application",
"app.startup.subscription -> app.application.subscription",
"app.startup.subscription -> app.application.subscription.complete",
"app.startup.subscription -> app.application.subscription.delete",
"app.startup.subscription -> app.application.subscription.mutation",
"app.startup.subscription -> app.application.subscription.write",
"app.startup.subscription -> app.db",
"app.startup.subscription -> app.db.oper",
"app.startup.subscription -> app.db.oper.subscribe",
"app.startup.subscription -> app.db.oper.subscribehistory",
"app.startup.subscription -> app.db.session",
"app.startup.subscription -> app.db.uow",
"app.startup.subscription -> app.runtime",
"app.startup.subscription -> app.runtime.events",
"app.startup.subscription -> app.schemas",
"app.startup.subscription -> app.schemas.types",
"app.startup.subscription -> app.startup",
"app.startup.subscription -> app.startup.outbox",
"app.startup.transaction -> app.db",
"app.startup.transaction -> app.db.uow",
"app.startup.transfer_initializer -> app.chain",
"app.startup.transfer_initializer -> app.chain.transfer",
"app.startup.workflow -> app.application",
"app.startup.workflow -> app.application.workflow",
"app.startup.workflow -> app.db",
"app.startup.workflow -> app.db.oper",
"app.startup.workflow -> app.db.oper.workflow",
"app.startup.workflow -> app.db.uow",
"app.startup.workflow_initializer -> app.workflow",
"app.testing -> app.testing.stub",
"app.testing.bootstrap -> app.application",
"app.testing.bootstrap -> app.application.site",
@@ -6322,9 +6352,10 @@
"app.testing.bootstrap -> app.db.oper.systemconfig",
"app.testing.bootstrap -> app.db.oper.userconfig",
"app.testing.bootstrap -> app.startup",
"app.testing.bootstrap -> app.startup.cache_initializer",
"app.testing.bootstrap -> app.startup.database_initializer",
"app.testing.bootstrap -> app.startup.domain_initializer",
"app.testing.bootstrap -> app.startup.initializers",
"app.testing.bootstrap -> app.startup.initializers.cache",
"app.testing.bootstrap -> app.startup.initializers.database",
"app.testing.bootstrap -> app.startup.initializers.domain",
"app.workflow -> app.application",
"app.workflow -> app.application.chain",
"app.workflow -> app.application.chain.data",
@@ -6500,7 +6531,7 @@
"app.workflow.actions.transfer_file -> app.workflow",
"app.workflow.actions.transfer_file -> app.workflow.actions"
],
"module_count": 800,
"module_count": 805,
"modules": [
"app",
"app.adapters",
@@ -6874,6 +6905,14 @@
"app.cli",
"app.command",
"app.db",
"app.db.adapters",
"app.db.adapters.chain",
"app.db.adapters.download",
"app.db.adapters.outbox",
"app.db.adapters.site",
"app.db.adapters.subscription",
"app.db.adapters.transaction",
"app.db.adapters.workflow",
"app.db.base",
"app.db.decorators",
"app.db.diagnostics",
@@ -7164,6 +7203,7 @@
"app.runtime.extensions.module_manager",
"app.runtime.extensions.plugin",
"app.runtime.extensions.plugin.access",
"app.runtime.extensions.plugin.admission",
"app.runtime.extensions.plugin.catalog",
"app.runtime.extensions.plugin.clone",
"app.runtime.extensions.plugin.contracts",
@@ -7256,31 +7296,27 @@
"app.sdk.string",
"app.sdk.utilities",
"app.startup",
"app.startup.agent_initializer",
"app.startup.cache_initializer",
"app.startup.chain_events",
"app.startup.command_initializer",
"app.startup.configuration",
"app.startup.context",
"app.startup.database",
"app.startup.database_initializer",
"app.startup.domain_initializer",
"app.startup.download_failure",
"app.startup.composition",
"app.startup.composition.configuration",
"app.startup.composition.context",
"app.startup.composition.database",
"app.startup.composition.subscription",
"app.startup.initializers",
"app.startup.initializers.agent",
"app.startup.initializers.cache",
"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.routers",
"app.startup.initializers.scheduler",
"app.startup.initializers.transfer",
"app.startup.initializers.workflow",
"app.startup.lifecycle",
"app.startup.lifecycle.components",
"app.startup.managed_resources_initializer",
"app.startup.modules_initializer",
"app.startup.monitor_initializer",
"app.startup.outbox",
"app.startup.plugins_initializer",
"app.startup.routers_initializer",
"app.startup.scheduler_initializer",
"app.startup.site",
"app.startup.subscription",
"app.startup.transaction",
"app.startup.transfer_initializer",
"app.startup.workflow",
"app.startup.workflow_initializer",
"app.testing",
"app.testing.bootstrap",
"app.testing.network_guard",
+13 -13
View File
@@ -187,8 +187,8 @@
"introduced": "v3.0.0",
"is_package": false,
"owner": "startup",
"replacement": "app.startup.database_initializer",
"target": "app.startup.database_initializer"
"replacement": "app.startup.initializers.database",
"target": "app.startup.initializers.database"
},
"app.db.mediaserver_oper": {
"introduced": "v3.0.0",
@@ -2029,7 +2029,7 @@
"count": 1
},
{
"caller": "app.startup.agent_initializer",
"caller": "app.startup.initializers.agent",
"count": 1
}
],
@@ -2043,7 +2043,7 @@
"count": 2
},
{
"caller": "app.startup.modules_initializer",
"caller": "app.startup.initializers.modules",
"count": 1
}
]
@@ -2194,7 +2194,7 @@
"count": 2
},
{
"caller": "app.startup.modules_initializer",
"caller": "app.startup.initializers.modules",
"count": 1
}
]
@@ -2203,11 +2203,11 @@
"consumers": [],
"producers": [
{
"caller": "app.startup.modules_initializer",
"caller": "app.startup.composition.subscription",
"count": 1
},
{
"caller": "app.startup.subscription",
"caller": "app.startup.initializers.modules",
"count": 1
}
]
@@ -2220,11 +2220,11 @@
"count": 1
},
{
"caller": "app.startup.modules_initializer",
"caller": "app.startup.composition.subscription",
"count": 1
},
{
"caller": "app.startup.subscription",
"caller": "app.startup.initializers.modules",
"count": 1
}
]
@@ -2241,11 +2241,11 @@
"count": 3
},
{
"caller": "app.startup.modules_initializer",
"caller": "app.startup.composition.subscription",
"count": 1
},
{
"caller": "app.startup.subscription",
"caller": "app.startup.initializers.modules",
"count": 1
}
]
@@ -2293,7 +2293,7 @@
"count": 2
},
{
"caller": "app.startup.modules_initializer",
"caller": "app.startup.initializers.modules",
"count": 1
}
]
@@ -2306,7 +2306,7 @@
"count": 2
},
{
"caller": "app.startup.modules_initializer",
"caller": "app.startup.initializers.modules",
"count": 1
}
]
+1 -1
View File
@@ -56,7 +56,7 @@ sites.__file__ = "<test-stub>"
sys.modules["app.application.site.sites"] = sites
from fastapi import FastAPI
from app.startup.routers_initializer import init_routers
from app.startup.initializers.routers import init_routers
app = FastAPI()
init_routers(app)
+1 -1
View File
@@ -9,7 +9,7 @@ from unittest.mock import AsyncMock, MagicMock
import pytest
from app.runtime.capabilities.errors import CapabilityRuntimeClosedError
from app.startup import agent_initializer
from app.startup.initializers import agent as agent_initializer
@pytest.mark.anyio
+1 -1
View File
@@ -64,7 +64,7 @@ def test_agent_initializer_import_only_registers_lazy_providers() -> None:
import json
import sys
import app.startup.agent_initializer
import app.startup.initializers.agent
forbidden = (
"app.agent.orchestrator",
+2 -1
View File
@@ -15,7 +15,8 @@ from app.agent.orchestrator import (
AgentManagerUnavailableError,
)
from app.agent.memory import MemoryManager
from app.startup import agent_initializer, modules_initializer
from app.startup.initializers import agent as agent_initializer
from app.startup.initializers import modules as modules_initializer
@pytest.mark.anyio
+6 -6
View File
@@ -512,7 +512,7 @@ def test_v1_model_free_routes_match_audited_native_allowlist():
def test_native_protocol_openapi_has_explicit_response_schemas():
"""OpenAI、Anthropic 与 MCP 原生协议响应必须在 OpenAPI 中明确建模。"""
from app.factory import create_app
from app.startup.routers_initializer import init_routers
from app.startup.initializers.routers import init_routers
app = create_app()
init_routers(app)
@@ -558,7 +558,7 @@ def test_native_protocol_openapi_has_explicit_response_schemas():
async def test_native_protocol_validation_errors_keep_native_shapes():
"""OpenAI 与 Anthropic 的请求校验错误应保持各自协议的错误结构。"""
from app.factory import create_app
from app.startup.routers_initializer import init_routers
from app.startup.initializers.routers import init_routers
app = create_app()
init_routers(app)
@@ -603,7 +603,7 @@ async def test_native_protocol_validation_errors_keep_native_shapes():
async def test_mcp_root_auth_error_keeps_jsonrpc_shape():
"""MCP 根端点的依赖异常应保持 JSON-RPC,REST 子端点仍由统一协议处理。"""
from app.factory import create_app
from app.startup.routers_initializer import init_routers
from app.startup.initializers.routers import init_routers
app = create_app()
init_routers(app)
@@ -678,7 +678,7 @@ async def test_native_ai_http_and_unhandled_errors_keep_protocol_shapes():
def test_servarr_and_cookiecloud_openapi_has_explicit_models():
"""兼容协议成功响应必须显式建模,错误响应必须声明统一结构。"""
from app.factory import create_app
from app.startup.routers_initializer import init_routers
from app.startup.initializers.routers import init_routers
app = create_app()
init_routers(app)
@@ -712,7 +712,7 @@ def test_servarr_and_cookiecloud_openapi_has_explicit_models():
def test_all_openapi_error_responses_use_json_schemas():
"""所有普通与原生协议错误响应都应在文档中声明 JSON 媒体类型和结构。"""
from app.factory import create_app
from app.startup.routers_initializer import init_routers
from app.startup.initializers.routers import init_routers
app = create_app()
init_routers(app)
@@ -739,7 +739,7 @@ def test_all_openapi_error_responses_use_json_schemas():
def test_openapi_success_models_have_no_implicit_empty_nested_schemas():
"""2xx 响应可达模型不得包含裸 Any、裸数组或未声明值类型的开放映射。"""
from app.factory import create_app
from app.startup.routers_initializer import init_routers
from app.startup.initializers.routers import init_routers
app = create_app()
init_routers(app)
+81
View File
@@ -12,6 +12,7 @@ IMPLEMENTATION_ROOTS = (
"app.agent.skills",
"app.adapters",
"app.application",
"app.db.adapters",
"app.domain",
"app.foundation",
"app.runtime",
@@ -66,6 +67,29 @@ RETIRED_CANONICAL_FILES = (
"app/adapters/network/sites.pyi",
"app/application/plugins.py",
"app/application/subscribe.py",
"app/startup/agent_initializer.py",
"app/startup/cache_initializer.py",
"app/startup/chain_events.py",
"app/startup/command_initializer.py",
"app/startup/configuration.py",
"app/startup/context.py",
"app/startup/database.py",
"app/startup/database_initializer.py",
"app/startup/domain_initializer.py",
"app/startup/download_failure.py",
"app/startup/managed_resources_initializer.py",
"app/startup/modules_initializer.py",
"app/startup/monitor_initializer.py",
"app/startup/outbox.py",
"app/startup/plugins_initializer.py",
"app/startup/routers_initializer.py",
"app/startup/scheduler_initializer.py",
"app/startup/site.py",
"app/startup/subscription.py",
"app/startup/transaction.py",
"app/startup/transfer_initializer.py",
"app/startup/workflow.py",
"app/startup/workflow_initializer.py",
)
PLUGIN_COMPONENT_ROOTS = (
"app/adapters/external/plugin",
@@ -288,6 +312,20 @@ def test_retired_canonical_filenames_do_not_return():
assert leftovers == []
def test_startup_root_contains_only_composition_packages():
"""组合根顶层只保留稳定分区,禁止再次堆叠扁平实现文件。"""
startup_root = APP_ROOT / "startup"
root_modules = sorted(path.name for path in startup_root.glob("*.py"))
python_packages = sorted(
path.name
for path in startup_root.iterdir()
if path.is_dir() and any(path.rglob("*.py"))
)
assert root_modules == ["__init__.py"]
assert python_packages == ["composition", "initializers", "lifecycle"]
def test_retired_canonical_roots_contain_no_python_sources():
"""已收敛的新增目录不得再次以顶级 Python 包形式出现。"""
leftovers = sorted(
@@ -394,6 +432,49 @@ def test_database_internals_do_not_import_db_facades():
assert violations == []
def test_base_crud_is_explicitly_legacy_only():
"""Base 便利 CRUD 只能保留兼容壳,不得伪装成新的正式事务入口。"""
path = APP_ROOT / "db" / "base.py"
tree = ast.parse(path.read_text(encoding="utf-8-sig"), filename=str(path))
base_class = next(
node
for node in tree.body
if isinstance(node, ast.ClassDef) and node.name == "Base"
)
formal_decorators = {
"db_query",
"db_update",
"async_db_query",
"async_db_update",
}
violations: list[str] = []
for node in base_class.body:
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
continue
decorators = {
decorator.id
for decorator in node.decorator_list
if isinstance(decorator, ast.Name)
}
if decorators & formal_decorators:
violations.append(node.name)
assert violations == []
def test_models_use_one_legacy_query_compatibility_shell():
"""旧 Model 查询统一使用 legacy 装饰器,不得再手写隐式会话 runner。"""
retired_names = {"run_legacy_sync_query", "run_legacy_async_query"}
violations: list[str] = []
for path in (APP_ROOT / "db" / "models").glob("*.py"):
tree = ast.parse(path.read_text(encoding="utf-8-sig"), filename=str(path))
if any(
isinstance(node, ast.Name) and node.id in retired_names
for node in ast.walk(tree)
):
violations.append(str(path.relative_to(PROJECT_ROOT)))
assert violations == []
def test_entry_layers_do_not_import_database_implementations():
"""API、应用、编排、Agent、监控、模块和 Runtime 只能经端口访问持久化。"""
graph = _build_module_graph()
+6 -6
View File
@@ -40,7 +40,7 @@ def test_clear_package_tool_cache_only_removes_pip_and_uv_old_files(tmp_path, mo
"""
包安装工具缓存清理只处理 pip/uv 子目录不接管整个 .cache 或业务缓存
"""
from app.startup.modules_initializer import clear_package_tool_cache
from app.startup.initializers.modules import clear_package_tool_cache
old_time = time.time() - 40 * 24 * 3600
cache_root = tmp_path / ".cache"
@@ -68,7 +68,7 @@ def test_clear_package_tool_cache_disabled_when_days_non_positive(tmp_path, monk
"""
PACKAGE_CACHE_DAYS 小于等于 0 时不清理包安装缓存
"""
from app.startup.modules_initializer import clear_package_tool_cache
from app.startup.initializers.modules import clear_package_tool_cache
old_time = time.time() - 40 * 24 * 3600
old_pip = tmp_path / ".cache" / "pip" / "old.whl"
@@ -88,7 +88,7 @@ def test_clear_package_tool_cache_isolates_subdir_errors(tmp_path, monkeypatch):
"""
单个工具缓存目录清理失败不影响另一个工具缓存目录
"""
from app.startup.modules_initializer import clear_package_tool_cache
from app.startup.initializers.modules import clear_package_tool_cache
calls = []
@@ -100,7 +100,7 @@ def test_clear_package_tool_cache_isolates_subdir_errors(tmp_path, monkeypatch):
monkeypatch.setattr(settings, "CONFIG_DIR", str(tmp_path))
monkeypatch.setattr(settings, "PACKAGE_CACHE_ROOT", str(tmp_path / "custom-package-cache"))
monkeypatch.setattr(settings, "PACKAGE_CACHE_DAYS", 30)
monkeypatch.setattr("app.startup.modules_initializer.SystemUtils.clear", fake_clear)
monkeypatch.setattr("app.startup.initializers.modules.SystemUtils.clear", fake_clear)
clear_package_tool_cache()
@@ -110,7 +110,7 @@ def test_clear_package_tool_cache_uses_package_cache_root(tmp_path, monkeypatch)
"""
PACKAGE_CACHE_ROOT 用作 pip/uv 清理根目录不扩大到配置目录下其他缓存
"""
from app.startup.modules_initializer import clear_package_tool_cache
from app.startup.initializers.modules import clear_package_tool_cache
old_time = time.time() - 40 * 24 * 3600
package_cache_root = tmp_path / "custom-package-cache"
@@ -134,7 +134,7 @@ def test_init_modules_does_not_clear_package_tool_cache(monkeypatch):
"""
包安装缓存清理由通用临时清理入口触发模块启动路径不直接执行清理
"""
from app.startup import modules_initializer
from app.startup.initializers import modules as modules_initializer
called = False
+1 -1
View File
@@ -22,7 +22,7 @@ from app.domain.metainfo import MetaInfo
from app.schemas.file import FileItem
from app.schemas.transfer import TransferInfo
from app.schemas.types import MediaSource, MediaType
from app.startup.chain_events import TransactionalChainDurableEventWriter
from app.db.adapters.chain import TransactionalChainDurableEventWriter
def _session_factory():
+1 -1
View File
@@ -4,7 +4,7 @@ from unittest.mock import AsyncMock
import pytest
from app.startup import modules_initializer
from app.startup.initializers import modules as modules_initializer
from app.startup.lifecycle import initialize_modules_component
from app.application.configuration import configure_runtime_settings
from app.runtime.settings import RuntimeSettingsCompat
+1 -1
View File
@@ -3,7 +3,7 @@ from types import SimpleNamespace
from unittest.mock import Mock
from app.application.database import get_database_governance
from app.startup import database as startup_database
from app.startup.composition import database as startup_database
def test_builder_uses_cached_engine_as_database_fact_source(monkeypatch) -> None:
+1 -1
View File
@@ -76,7 +76,7 @@ from sqlalchemy.exc import IntegrityError
from app.runtime.config import settings
from app.db import get_engine
from app.startup.database_initializer import init_db, update_db
from app.startup.initializers.database import init_db, update_db
media_tables = {media_tables!r}
legacy_identity_columns = {legacy_identity_columns!r}
+2 -2
View File
@@ -27,8 +27,8 @@ from sqlalchemy import (
)
from sqlalchemy.engine.url import make_url
from app.startup import database_initializer as db_init
from app.startup import database as startup_database
from app.startup.composition import database as startup_database
from app.startup.initializers import database as db_init
from app.startup import lifecycle
from app.runtime.health import get_application_health
from app.db.models.systemconfig import SystemConfig
+2 -6
View File
@@ -292,13 +292,9 @@ def test_passkey_lookup_by_credential_id_skips_inactive(db):
assert asyncio.run(PassKey.async_get_by_credential_id(credential_id="cred-dead")) is None
def test_passkey_model_sync_queries_keep_no_session_plugin_abi(db, monkeypatch):
"""旧插件不传 Session 时仍应获得短会话查询,而不恢复 Model 装饰器"""
def test_passkey_model_sync_queries_keep_no_session_plugin_abi(db):
"""旧插件不传 Session 时仍由统一 legacy 装饰器获得短会话查询"""
db.add(_passkey(9004, "cred-legacy"))
monkeypatch.setattr(
"app.db.models.passkey.run_legacy_sync_query",
lambda operation: operation(db.session),
)
assert [item.credential_id for item in PassKey.get_by_user_id(user_id=9004)] == [
"cred-legacy"
@@ -352,13 +352,10 @@ def test_agenttask_get_for_user_enforces_ownership(db):
assert AgentTask.get_for_user(db.session, task_id, user_id="bob") is None
def test_agenttask_model_queries_keep_no_session_plugin_abi(db, monkeypatch):
"""旧插件省略 Session 时仍可按原关键字参数查询 Agent 任务"""
def test_agenttask_model_queries_keep_no_session_plugin_abi(db):
"""旧插件省略 Session 时仍由统一 legacy 装饰器按原参数查询"""
task_id = AgentTask.add_task(db.session, **_task("legacy", user_id="legacy-user"))
monkeypatch.setattr(
"app.db.models.agenttask.run_legacy_sync_query",
lambda operation: operation(db.session),
)
db.session.commit()
assert AgentTask.get_for_user(
task_id=task_id,
+1 -1
View File
@@ -12,7 +12,7 @@ from app.runtime.event.binding import (
from app.runtime.event.errors import EventErrorPolicy
from app.runtime.events import Event
from app.schemas.types import EventType
from app.startup.modules_initializer import get_host_event_handler_factories
from app.startup.initializers.modules import get_host_event_handler_factories
class _UnmanagedHandler:
+1 -1
View File
@@ -15,7 +15,7 @@ from app.api.context import (
)
from app.api.dependencies.agent import get_agent_chat_persistence
from app.startup import lifecycle
from app.startup.context import (
from app.startup.composition.context import (
AgentChatRuntime,
AuthenticationRuntime,
HistoryRuntime,
+1 -1
View File
@@ -17,7 +17,7 @@ from app.runtime.compat.resource_imports import (
)
from app.runtime.extensions import plugin_manager as plugin_manager_module
from app.runtime.extensions.plugin_manager import PluginManager
from app.startup import plugins_initializer
from app.startup.initializers import plugins as plugins_initializer
_HEADED_CLOAKBROWSER_ENTRYPOINTS = (
+2 -1
View File
@@ -6,7 +6,8 @@ from unittest.mock import AsyncMock, MagicMock
import pytest
from fastapi import FastAPI
from app.startup import lifecycle, modules_initializer
from app.startup import lifecycle
from app.startup.initializers import modules as modules_initializer
from app.adapters.network import http as http_utils
from app.runtime.tasks import get_task_registry
+2 -2
View File
@@ -294,7 +294,7 @@ def test_startup_initializer_discovers_manifest_without_importing_resource() ->
script = """
import asyncio
import sys
from app.startup.managed_resources_initializer import (
from app.startup.initializers.managed_resources import (
init_managed_resources,
stop_managed_resources,
)
@@ -320,7 +320,7 @@ assert "pyvirtualdisplay" not in sys.modules
def test_startup_shutdown_without_init_does_not_build_registry(monkeypatch) -> None:
"""未执行启动装配时,关闭入口不得通过发现声明反向初始化 Runtime。"""
from app.startup import managed_resources_initializer
from app.startup.initializers import managed_resources as managed_resources_initializer
build_registry = MagicMock(side_effect=AssertionError("must not discover"))
monkeypatch.setattr(
+1 -1
View File
@@ -12,7 +12,7 @@ import pytest
from app.monitor.monitor import Monitor
from app.monitor.recovery import RecoveryExecutor, RecoveryState
from app.foundation.singleton import SingletonClass
from app.startup.monitor_initializer import init_monitor, stop_monitor
from app.startup.initializers.monitor import init_monitor, stop_monitor
def _build_monitor() -> Monitor:
+1 -1
View File
@@ -21,7 +21,7 @@ from app.runtime.extensions.plugin.admission import PluginMutationAdmission
from app.runtime.extensions.plugin.system import reset_plugin_system
from app.runtime.extensions.plugin_manager import PluginManager
from app.schemas.plugin import PluginRuntimeStatus
from app.startup import plugins_initializer
from app.startup.initializers import plugins as plugins_initializer
def _reset_plugin_manager() -> None:
+1 -1
View File
@@ -5,7 +5,7 @@ from app.adapters.system.resource import (
ResourceHelper,
configure_resource_version_provider,
)
from app.startup import modules_initializer
from app.startup.initializers import modules as modules_initializer
ROOT_DIR = Path(__file__).resolve().parents[1]
+4 -4
View File
@@ -56,7 +56,7 @@ def _route_contract(route: APIRoute) -> tuple[Any, ...]:
def test_init_routers_directly_includes_endpoint_router_specs(monkeypatch):
"""启动聚合应直接 include 原始端点路由器并一次性附加完整 v1 前缀。"""
from app.api.router_specs import API_V1_ROUTER_SPECS
from app.startup.routers_initializer import init_routers
from app.startup.initializers.routers import init_routers
app = FastAPI()
include_calls = []
@@ -90,7 +90,7 @@ def test_init_routers_directly_includes_endpoint_router_specs(monkeypatch):
def test_direct_v1_routes_and_openapi_match_compatibility_router():
"""最终应用的 v1 路由合同与 OpenAPI 应和兼容聚合结果完全一致。"""
from app.api.apiv1 import api_router
from app.startup.routers_initializer import init_routers
from app.startup.initializers.routers import init_routers
compatibility_app = FastAPI()
compatibility_app.include_router(api_router, prefix=settings.API_V1_STR)
@@ -123,7 +123,7 @@ def test_direct_v1_routes_and_openapi_match_compatibility_router():
@pytest.mark.anyio
async def test_direct_routes_honor_application_dependency_overrides():
"""直接聚合后的路由仍应由最终 FastAPI 应用解析依赖覆盖。"""
from app.startup.routers_initializer import init_routers
from app.startup.initializers.routers import init_routers
app = FastAPI()
init_routers(app)
@@ -163,7 +163,7 @@ def test_compatibility_api_router_keeps_public_contract():
def test_init_routers_accepts_composition_root_api_prefix():
"""路由初始化应使用组合根传入的 API 前缀。"""
from app.startup.routers_initializer import init_routers
from app.startup.initializers.routers import init_routers
app = FastAPI()
init_routers(app, "/custom/v1")
+1 -1
View File
@@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, Mock
from app import scheduler as scheduler_module
from app.scheduler import Scheduler
from app.application.configuration import SchedulerRuntimeConfig
from app.startup import scheduler_initializer
from app.startup.initializers import scheduler as scheduler_initializer
class _BackgroundSchedulerStub:
+3 -3
View File
@@ -4,7 +4,7 @@ from unittest.mock import MagicMock, patch
import pytest
from app.startup.download_failure import TransactionalDownloadFailureRepository
from app.db.adapters.download import TransactionalDownloadFailureRepository
def _session_factory(session: MagicMock):
@@ -22,7 +22,7 @@ def test_record_failure_commits_explicit_unit_of_work() -> None:
repository = TransactionalDownloadFailureRepository(_session_factory(session))
with patch(
"app.startup.download_failure.DownloadFailureOper",
"app.db.adapters.download.DownloadFailureOper",
return_value=oper,
):
result = repository.record_failure("fp", "now", "next", title="片名")
@@ -40,7 +40,7 @@ def test_record_failure_rolls_back_explicit_unit_of_work() -> None:
repository = TransactionalDownloadFailureRepository(_session_factory(session))
with patch(
"app.startup.download_failure.DownloadFailureOper",
"app.db.adapters.download.DownloadFailureOper",
return_value=oper,
), pytest.raises(ValueError, match="duplicate"):
repository.record_failure("fp", "now", "next")
+1 -1
View File
@@ -15,7 +15,7 @@ from app.foundation.singleton import Singleton
from app.runtime.config import global_vars
from app.application.transfer import TransferQueue, TransferTask
from app.schemas.file import FileItem
from app.startup import transfer_initializer
from app.startup.initializers import transfer as transfer_initializer
def _build_chain(*, transfer_threads: int = 0) -> TransferChain:
+4 -4
View File
@@ -28,10 +28,10 @@ def test_mypy_gate_has_explicit_strict_scope_without_global_ignore() -> None:
assert "app/runtime/event/errors.py" in governed_files
assert "app/application/scheduling.py" in governed_files
assert "scripts/architecture/async_blocking.py" in governed_files
assert "app/startup/context.py" in governed_files
assert "app/startup/configuration.py" in governed_files
assert "app/startup/download_failure.py" in governed_files
assert "app/startup/workflow.py" in governed_files
assert "app/startup/composition/context.py" in governed_files
assert "app/startup/composition/configuration.py" in governed_files
assert "app/db/adapters/download.py" in governed_files
assert "app/db/adapters/workflow.py" in governed_files
assert "app/application/workflow.py" in governed_files
assert "app/api/context.py" in governed_files
assert "app/db/base.py" in governed_files