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
+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()