mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 15:38:19 +08:00
refactor: reorganize startup persistence boundaries
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""实现 Application 持久化端口的 SQLAlchemy 适配器。"""
|
||||
@@ -0,0 +1,152 @@
|
||||
"""Chain durable 事件写入端口的 SQLAlchemy 适配器。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.application.chain.durable_events import (
|
||||
ChainDurableEventWriter,
|
||||
TransferHistoryRef,
|
||||
download_added_event_key,
|
||||
snapshot_download_added,
|
||||
snapshot_transfer_result,
|
||||
transfer_result_event_key,
|
||||
)
|
||||
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
|
||||
|
||||
|
||||
class _StagingTransferHistoryWriter:
|
||||
"""让既有历史字段映射复用无提交的 replace 适配器。"""
|
||||
|
||||
def __init__(self, repository: TransferHistoryOper) -> None:
|
||||
"""保存绑定调用方 Session 的整理历史仓储。"""
|
||||
self._repository = repository
|
||||
|
||||
def get_by_src(
|
||||
self,
|
||||
src: str,
|
||||
storage: str | None = None,
|
||||
) -> TransferHistoryRecord | None:
|
||||
"""转发按源路径读取。"""
|
||||
return self._repository.get_by_src(src, storage)
|
||||
|
||||
def get_success_by_src(
|
||||
self,
|
||||
src: str,
|
||||
storage: str | None = None,
|
||||
) -> TransferHistoryRecord | None:
|
||||
"""转发按源路径读取成功记录。"""
|
||||
return self._repository.get_success_by_src(src, storage)
|
||||
|
||||
def add_force(self, **payload: Any) -> TransferHistoryRecord:
|
||||
"""保持应用层旧端口名,但只暂存替换而不自行提交。"""
|
||||
return self._repository.stage_replace_by_src(**payload)
|
||||
|
||||
|
||||
class TransactionalChainDurableEventWriter(ChainDurableEventWriter):
|
||||
"""为每次 Chain 结果事件创建独占同步 Session 和 UoW。"""
|
||||
|
||||
def __init__(self, session_factory: Callable[[], Session]) -> None:
|
||||
"""注入惰性同步 Session 工厂。"""
|
||||
self._session_factory = session_factory
|
||||
|
||||
def download_added(
|
||||
self,
|
||||
*,
|
||||
history_payload: dict[str, Any],
|
||||
file_payloads: list[dict[str, Any]],
|
||||
event_payload: dict[str, Any],
|
||||
after_commit: Callable[[], None],
|
||||
publish: Callable[[dict[str, Any]], None],
|
||||
) -> None:
|
||||
"""原子写下载历史、文件清单和 DownloadAdded intent。"""
|
||||
session = self._session_factory()
|
||||
try:
|
||||
repository = DownloadHistoryOper(session)
|
||||
outbox = SqlAlchemyOutboxRepository(session)
|
||||
command = DurableEventCommand(
|
||||
unit_of_work=SqlAlchemyUnitOfWork(session),
|
||||
outbox=outbox,
|
||||
)
|
||||
event_key = download_added_event_key(event_payload)
|
||||
event_payload["idempotency_key"] = event_key
|
||||
|
||||
def stage_business() -> None:
|
||||
"""在同一事务暂存下载历史和可选文件清单。"""
|
||||
repository.stage_add(history_payload)
|
||||
if file_payloads:
|
||||
repository.stage_add_files(file_payloads)
|
||||
|
||||
command.execute(
|
||||
intent=OutboxIntent(
|
||||
event_key=event_key,
|
||||
topic="download.added",
|
||||
payload=snapshot_download_added(event_payload),
|
||||
),
|
||||
stage_business=stage_business,
|
||||
after_commit=after_commit,
|
||||
publish=lambda: publish(event_payload),
|
||||
)
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
def transfer_result(
|
||||
self,
|
||||
*,
|
||||
topic: str,
|
||||
stage_history: Callable[[TransferHistoryWriter], TransferHistoryRecord | None],
|
||||
event_payload: dict[str, Any],
|
||||
publish: Callable[[dict[str, Any]], None],
|
||||
) -> TransferHistoryRecord | None:
|
||||
"""原子写整理历史与结果 intent,并返回脱离 Session 的最小投影。"""
|
||||
session = self._session_factory()
|
||||
try:
|
||||
staging = _StagingTransferHistoryWriter(TransferHistoryOper(session))
|
||||
command = DurableEventCommand(
|
||||
unit_of_work=SqlAlchemyUnitOfWork(session),
|
||||
outbox=SqlAlchemyOutboxRepository(session),
|
||||
)
|
||||
|
||||
def stage_business() -> TransferHistoryRef | None:
|
||||
"""复用历史字段映射,并在 flush 后冻结安全投影。"""
|
||||
history = stage_history(staging)
|
||||
if history is None:
|
||||
return None
|
||||
return TransferHistoryRef(
|
||||
id=history.id,
|
||||
status=bool(history.status),
|
||||
src=history.src,
|
||||
src_storage=history.src_storage,
|
||||
src_fileitem=history.src_fileitem,
|
||||
)
|
||||
|
||||
def build_intent(
|
||||
history: TransferHistoryRef | None,
|
||||
) -> OutboxIntent:
|
||||
"""历史 ID 确定后构造事件键与可恢复快照。"""
|
||||
if history is None:
|
||||
raise RuntimeError("整理历史暂存失败,无法登记 durable 结果事件")
|
||||
event_key = transfer_result_event_key(topic, history.id)
|
||||
event_payload["transfer_history_id"] = history.id
|
||||
event_payload["idempotency_key"] = event_key
|
||||
return OutboxIntent(
|
||||
event_key=event_key,
|
||||
topic=topic,
|
||||
payload=snapshot_transfer_result(event_payload),
|
||||
)
|
||||
|
||||
return command.execute(
|
||||
intent=build_intent,
|
||||
stage_business=stage_business,
|
||||
publish=lambda: publish(event_payload),
|
||||
)
|
||||
finally:
|
||||
session.close()
|
||||
@@ -0,0 +1,55 @@
|
||||
"""下载失败冷却切片的显式会话与事务适配器。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any, cast
|
||||
|
||||
from app.db.oper.downloadfailure import DownloadFailureOper
|
||||
from app.db.uow import SqlAlchemyUnitOfWork
|
||||
|
||||
|
||||
class TransactionalDownloadFailureRepository:
|
||||
"""为 Chain 下载失败读写创建短生命周期会话并显式收口事务。"""
|
||||
|
||||
def __init__(self, session_factory: Callable[[], Any]) -> None:
|
||||
"""保存由启动组合根提供的同步会话工厂。"""
|
||||
self._session_factory = session_factory
|
||||
|
||||
def get_active_by_fingerprints(
|
||||
self,
|
||||
fingerprints: list[str],
|
||||
now_time: str,
|
||||
) -> dict[str, Any]:
|
||||
"""在独立只读会话中查询仍处于冷却期的失败记录。"""
|
||||
with self._session_factory() as session:
|
||||
return cast(
|
||||
dict[str, Any],
|
||||
DownloadFailureOper(db=session).get_active_by_fingerprints(
|
||||
fingerprints=fingerprints,
|
||||
now_time=now_time,
|
||||
),
|
||||
)
|
||||
|
||||
def record_failure(
|
||||
self,
|
||||
fingerprint: str,
|
||||
now_time: str,
|
||||
next_retry_at: str,
|
||||
**kwargs: object,
|
||||
) -> Any:
|
||||
"""在一个显式 UoW 中新增或更新下载失败记录。"""
|
||||
with self._session_factory() as session:
|
||||
transaction = SqlAlchemyUnitOfWork(session)
|
||||
try:
|
||||
failure = DownloadFailureOper(db=session).record_failure(
|
||||
fingerprint=fingerprint,
|
||||
now_time=now_time,
|
||||
next_retry_at=next_retry_at,
|
||||
**kwargs,
|
||||
)
|
||||
transaction.commit()
|
||||
return failure
|
||||
except Exception:
|
||||
transaction.rollback()
|
||||
raise
|
||||
@@ -0,0 +1,168 @@
|
||||
"""Application outbox 端口的 SQLAlchemy 持久化适配器。"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import or_, select, update
|
||||
from sqlalchemy.orm import Session
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from app.application.outbox import ClaimedOutboxMessage, OutboxIntent
|
||||
from app.db.base import execute_dml
|
||||
from app.db.models.outbox import OutboxMessage
|
||||
|
||||
|
||||
def _iso(value: datetime) -> str:
|
||||
"""将带时区时间统一序列化为可排序 ISO 字符串。"""
|
||||
return value.isoformat()
|
||||
|
||||
|
||||
class SqlAlchemyOutboxRepository:
|
||||
"""使用调用方 Session 原子暂存并条件认领 outbox。"""
|
||||
|
||||
def __init__(self, session: Session) -> None:
|
||||
"""保存由调用方拥有的 SQLAlchemy Session。"""
|
||||
self._session = session
|
||||
|
||||
def stage(self, intent: OutboxIntent, now: datetime) -> None:
|
||||
"""加入当前事务并 flush,使唯一键冲突在业务 commit 前暴露。"""
|
||||
self._session.add(
|
||||
OutboxMessage(
|
||||
event_key=intent.event_key,
|
||||
topic=intent.topic,
|
||||
payload_version=intent.payload_version,
|
||||
payload=intent.payload,
|
||||
status="pending",
|
||||
attempt=0,
|
||||
next_retry_at=_iso(now),
|
||||
created_at=_iso(now),
|
||||
)
|
||||
)
|
||||
self._session.flush()
|
||||
|
||||
def claim(
|
||||
self,
|
||||
now: datetime,
|
||||
lease_until: datetime,
|
||||
) -> ClaimedOutboxMessage | None:
|
||||
"""条件更新候选行;并发丢失竞争时返回 None。"""
|
||||
now_text = _iso(now)
|
||||
candidate = self._session.execute(
|
||||
select(OutboxMessage)
|
||||
.where(
|
||||
OutboxMessage.status.in_(("pending", "processing")),
|
||||
OutboxMessage.next_retry_at <= now_text,
|
||||
or_(
|
||||
OutboxMessage.lease_until.is_(None),
|
||||
OutboxMessage.lease_until <= now_text,
|
||||
),
|
||||
)
|
||||
.order_by(OutboxMessage.id)
|
||||
.limit(1)
|
||||
).scalars().first()
|
||||
if candidate is None:
|
||||
return None
|
||||
next_attempt = candidate.attempt + 1
|
||||
claimed = execute_dml(
|
||||
self._session,
|
||||
update(OutboxMessage)
|
||||
.where(
|
||||
OutboxMessage.id == candidate.id,
|
||||
OutboxMessage.attempt == candidate.attempt,
|
||||
or_(
|
||||
OutboxMessage.lease_until.is_(None),
|
||||
OutboxMessage.lease_until <= now_text,
|
||||
),
|
||||
)
|
||||
.values(
|
||||
status="processing",
|
||||
attempt=next_attempt,
|
||||
lease_until=_iso(lease_until),
|
||||
),
|
||||
)
|
||||
self._session.commit()
|
||||
if not claimed:
|
||||
return None
|
||||
return ClaimedOutboxMessage(
|
||||
message_id=candidate.id,
|
||||
event_key=candidate.event_key,
|
||||
topic=candidate.topic,
|
||||
payload=dict(candidate.payload),
|
||||
payload_version=candidate.payload_version,
|
||||
attempt=next_attempt,
|
||||
)
|
||||
|
||||
def complete(self, message_id: int, completed_at: datetime) -> None:
|
||||
"""持久化完成终态并释放 lease。"""
|
||||
self._session.execute(
|
||||
update(OutboxMessage)
|
||||
.where(OutboxMessage.id == message_id)
|
||||
.values(status="completed", completed_at=_iso(completed_at), lease_until=None)
|
||||
)
|
||||
self._session.commit()
|
||||
|
||||
def complete_by_event_key(self, event_key: str, completed_at: datetime) -> None:
|
||||
"""即时 post-commit 全部成功时按幂等键收口对应 intent。"""
|
||||
self._session.execute(
|
||||
update(OutboxMessage)
|
||||
.where(OutboxMessage.event_key == event_key)
|
||||
.values(status="completed", completed_at=_iso(completed_at), lease_until=None)
|
||||
)
|
||||
self._session.commit()
|
||||
|
||||
def retry(
|
||||
self,
|
||||
message_id: int,
|
||||
*,
|
||||
next_retry_at: datetime,
|
||||
last_error: str,
|
||||
dead: bool,
|
||||
) -> None:
|
||||
"""持久化下一次退避或不可自动重试的 dead 终态。"""
|
||||
self._session.execute(
|
||||
update(OutboxMessage)
|
||||
.where(OutboxMessage.id == message_id)
|
||||
.values(
|
||||
status="dead" if dead else "pending",
|
||||
next_retry_at=_iso(next_retry_at),
|
||||
lease_until=None,
|
||||
last_error=last_error,
|
||||
)
|
||||
)
|
||||
self._session.commit()
|
||||
|
||||
|
||||
class SqlAlchemyAsyncOutboxStager:
|
||||
"""只负责把 outbox 意图加入调用方异步事务。"""
|
||||
|
||||
def __init__(self, session: AsyncSession) -> None:
|
||||
"""保存由异步订阅命令拥有的 Session。"""
|
||||
self._session = session
|
||||
|
||||
async def stage(self, intent: OutboxIntent, now: datetime) -> None:
|
||||
"""暂存并 flush,确保业务行与意图由同一次 commit 决定。"""
|
||||
self._session.add(
|
||||
OutboxMessage(
|
||||
event_key=intent.event_key,
|
||||
topic=intent.topic,
|
||||
payload_version=intent.payload_version,
|
||||
payload=intent.payload,
|
||||
status="pending",
|
||||
attempt=0,
|
||||
next_retry_at=_iso(now),
|
||||
created_at=_iso(now),
|
||||
)
|
||||
)
|
||||
await self._session.flush()
|
||||
|
||||
async def complete_by_event_key(
|
||||
self,
|
||||
event_key: str,
|
||||
completed_at: datetime,
|
||||
) -> None:
|
||||
"""异步 post-commit 全部成功时按幂等键收口 intent。"""
|
||||
await self._session.execute(
|
||||
update(OutboxMessage)
|
||||
.where(OutboxMessage.event_key == event_key)
|
||||
.values(status="completed", completed_at=_iso(completed_at), lease_until=None)
|
||||
)
|
||||
await self._session.commit()
|
||||
@@ -0,0 +1,225 @@
|
||||
"""站点 Chain 端口的显式会话与事务适配器。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from contextlib import AbstractAsyncContextManager
|
||||
from typing import Any, TypeVar
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db.oper.site import SiteOper
|
||||
from app.db.uow import SqlAlchemyAsyncUnitOfWork, SqlAlchemyUnitOfWork
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class TransactionalSiteRepository:
|
||||
"""为同步 Chain 站点端口和异步健康统计提供短生命周期会话。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
sync_session: Callable[[], Session],
|
||||
async_session: Callable[[], AbstractAsyncContextManager[AsyncSession]],
|
||||
) -> None:
|
||||
"""保存同步会话工厂和异步会话上下文工厂。"""
|
||||
self._sync_session = sync_session
|
||||
self._async_session = async_session
|
||||
|
||||
def _read(self, operation: Callable[[SiteOper], T]) -> T:
|
||||
"""在独立同步会话中执行只读站点操作。"""
|
||||
with self._sync_session() as session:
|
||||
return operation(SiteOper(db=session))
|
||||
|
||||
def _write(self, operation: Callable[[SiteOper], T]) -> T:
|
||||
"""在独立同步 UoW 中执行站点写操作。"""
|
||||
with self._sync_session() as session:
|
||||
session.expire_on_commit = False
|
||||
unit_of_work = SqlAlchemyUnitOfWork(session)
|
||||
try:
|
||||
result = operation(SiteOper(db=session))
|
||||
unit_of_work.commit()
|
||||
return result
|
||||
except Exception:
|
||||
unit_of_work.rollback()
|
||||
raise
|
||||
|
||||
async def _async_write(
|
||||
self,
|
||||
operation: Callable[[SiteOper], Awaitable[T]],
|
||||
) -> T:
|
||||
"""在独立异步 UoW 中执行站点写操作。"""
|
||||
async with self._async_session() as session:
|
||||
session.sync_session.expire_on_commit = False
|
||||
unit_of_work = SqlAlchemyAsyncUnitOfWork(session)
|
||||
try:
|
||||
result = await operation(SiteOper(db=session))
|
||||
await unit_of_work.commit()
|
||||
return result
|
||||
except Exception:
|
||||
await unit_of_work.rollback()
|
||||
raise
|
||||
|
||||
async def _async_read(self, operation: Callable[[SiteOper], Awaitable[T]]) -> T:
|
||||
"""在独立异步会话中执行只读站点操作。"""
|
||||
async with self._async_session() as session:
|
||||
return await operation(SiteOper(db=session))
|
||||
|
||||
def add(self, **kwargs: Any) -> tuple[bool, str]:
|
||||
"""新增站点并提交事务。"""
|
||||
return self._write(lambda repository: repository.add(**kwargs))
|
||||
|
||||
def get(self, site_id: int) -> Any:
|
||||
"""按 ID 查询站点。"""
|
||||
return self._read(lambda repository: repository.get(site_id))
|
||||
|
||||
def get_by_domain(self, domain: str) -> Any:
|
||||
"""按域名查询站点。"""
|
||||
return self._read(lambda repository: repository.get_by_domain(domain))
|
||||
|
||||
def get_domains_by_ids(self, ids: list[int]) -> list[str | None]:
|
||||
"""查询一组站点 ID 对应的域名。"""
|
||||
return self._read(lambda repository: repository.get_domains_by_ids(ids))
|
||||
|
||||
def list(self) -> list[Any]:
|
||||
"""查询全部站点。"""
|
||||
return self._read(lambda repository: repository.list())
|
||||
|
||||
def list_order_by_pri(self) -> list[Any]:
|
||||
"""同步按优先级查询站点。"""
|
||||
return self._read(lambda repository: repository.list_order_by_pri())
|
||||
|
||||
def get_userdata_latest(self) -> list[Any]:
|
||||
"""同步查询各站点最新用户数据。"""
|
||||
return self._read(lambda repository: repository.get_userdata_latest())
|
||||
|
||||
async def async_get(self, site_id: int) -> Any:
|
||||
"""异步按 ID 查询站点。"""
|
||||
return await self._async_read(lambda repository: repository.async_get(site_id))
|
||||
|
||||
async def async_get_by_domain(self, domain: str) -> Any:
|
||||
"""异步按域名查询站点。"""
|
||||
return await self._async_read(
|
||||
lambda repository: repository.async_get_by_domain(domain)
|
||||
)
|
||||
|
||||
async def async_get_by_name(self, name: str) -> Any:
|
||||
"""异步按名称查询站点。"""
|
||||
return await self._async_read(
|
||||
lambda repository: repository.async_get_by_name(name)
|
||||
)
|
||||
|
||||
async def async_list(self) -> list[Any]:
|
||||
"""异步查询全部站点。"""
|
||||
return await self._async_read(lambda repository: repository.async_list())
|
||||
|
||||
async def async_list_order_by_pri(self) -> list[Any]:
|
||||
"""异步按优先级查询站点。"""
|
||||
return await self._async_read(
|
||||
lambda repository: repository.async_list_order_by_pri()
|
||||
)
|
||||
|
||||
async def async_update(self, site_id: int, payload: dict[str, Any]) -> Any:
|
||||
"""异步更新站点并提交事务。"""
|
||||
return await self._async_write(
|
||||
lambda repository: repository.async_update(site_id, payload)
|
||||
)
|
||||
|
||||
async def async_get_userdata_by_domain(
|
||||
self,
|
||||
domain: str,
|
||||
workdate: str | None = None,
|
||||
) -> list[Any]:
|
||||
"""异步查询站点用户数据。"""
|
||||
return await self._async_read(
|
||||
lambda repository: repository.async_get_userdata_by_domain(domain, workdate)
|
||||
)
|
||||
|
||||
async def async_get_userdata_latest(self) -> list[Any]:
|
||||
"""异步查询各站点最新用户数据。"""
|
||||
return await self._async_read(
|
||||
lambda repository: repository.async_get_userdata_latest()
|
||||
)
|
||||
|
||||
async def async_get_icon_by_domain(self, domain: str) -> Any:
|
||||
"""异步按域名查询站点图标。"""
|
||||
return await self._async_read(
|
||||
lambda repository: repository.async_get_icon_by_domain(domain)
|
||||
)
|
||||
|
||||
async def async_get_statistic_by_domain(self, domain: str) -> Any:
|
||||
"""异步按域名查询站点统计。"""
|
||||
return await self._async_read(
|
||||
lambda repository: repository.async_get_statistic_by_domain(domain)
|
||||
)
|
||||
|
||||
async def async_list_statistics(self) -> list[Any]:
|
||||
"""异步查询全部站点统计。"""
|
||||
return await self._async_read(
|
||||
lambda repository: repository.async_list_statistics()
|
||||
)
|
||||
|
||||
def update(self, site_id: int, payload: dict[str, Any]) -> Any:
|
||||
"""更新站点并提交事务。"""
|
||||
return self._write(lambda repository: repository.update(site_id, payload))
|
||||
|
||||
def update_cookie(self, domain: str, cookies: str) -> tuple[bool, str]:
|
||||
"""更新站点 Cookie 并提交事务。"""
|
||||
return self._write(
|
||||
lambda repository: repository.update_cookie(domain, cookies)
|
||||
)
|
||||
|
||||
def update_rss(self, domain: str, rss: str) -> tuple[bool, str]:
|
||||
"""更新站点 RSS 地址并提交事务。"""
|
||||
return self._write(lambda repository: repository.update_rss(domain, rss))
|
||||
|
||||
def update_userdata(
|
||||
self,
|
||||
domain: str,
|
||||
name: str,
|
||||
payload: dict[str, Any],
|
||||
) -> tuple[bool, str]:
|
||||
"""更新站点用户数据并提交事务。"""
|
||||
return self._write(
|
||||
lambda repository: repository.update_userdata(domain, name, payload)
|
||||
)
|
||||
|
||||
def update_icon(
|
||||
self,
|
||||
name: str,
|
||||
domain: str,
|
||||
icon_url: str,
|
||||
icon_base64: str,
|
||||
) -> bool:
|
||||
"""更新站点图标并提交事务。"""
|
||||
return self._write(
|
||||
lambda repository: repository.update_icon(
|
||||
name,
|
||||
domain,
|
||||
icon_url,
|
||||
icon_base64,
|
||||
)
|
||||
)
|
||||
|
||||
def success(self, domain: str, seconds: int | None = None) -> Any:
|
||||
"""记录站点访问成功并提交事务。"""
|
||||
return self._write(lambda repository: repository.success(domain, seconds))
|
||||
|
||||
def fail(self, domain: str) -> Any:
|
||||
"""记录站点访问失败并提交事务。"""
|
||||
return self._write(lambda repository: repository.fail(domain))
|
||||
|
||||
async def async_success(self, domain: str, seconds: int | None = None) -> Any:
|
||||
"""异步记录站点访问成功并提交事务。"""
|
||||
return await self._async_write(
|
||||
lambda repository: repository.async_success(domain, seconds)
|
||||
)
|
||||
|
||||
async def async_fail(self, domain: str) -> Any:
|
||||
"""异步记录站点访问失败并提交事务。"""
|
||||
return await self._async_write(
|
||||
lambda repository: repository.async_fail(domain)
|
||||
)
|
||||
@@ -0,0 +1,129 @@
|
||||
"""订阅写入端口的 SQLAlchemy 事务适配器。"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from contextlib import AbstractAsyncContextManager
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.application.subscription.write import (
|
||||
AfterCommitEffect,
|
||||
AsyncAfterCommitEffect,
|
||||
AsyncCreateSubscriptionCommand,
|
||||
CreateSubscriptionCommand,
|
||||
subscription_added_event_key,
|
||||
subscription_added_notification_key,
|
||||
subscription_added_report_key,
|
||||
)
|
||||
from app.db.adapters.outbox import (
|
||||
SqlAlchemyAsyncOutboxStager,
|
||||
SqlAlchemyOutboxRepository,
|
||||
)
|
||||
from app.db.oper.subscribe import SubscribeOper
|
||||
from app.db.uow import SqlAlchemyAsyncUnitOfWork, SqlAlchemyUnitOfWork
|
||||
|
||||
|
||||
class TransactionalSubscribeWriter:
|
||||
"""为每次订阅新增创建独占会话,并把提交权交给 Application Command。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
sync_session: Callable[[], Session],
|
||||
async_session: Callable[
|
||||
[],
|
||||
AbstractAsyncContextManager[AsyncSession],
|
||||
],
|
||||
) -> None:
|
||||
"""注入同步会话工厂和异步会话作用域。"""
|
||||
self._sync_session = sync_session
|
||||
self._async_session = async_session
|
||||
|
||||
def add(
|
||||
self,
|
||||
identity: dict,
|
||||
payload: dict,
|
||||
username: str | None = None,
|
||||
after_commit: AfterCommitEffect | None = None,
|
||||
notification: dict[str, object] | None = None,
|
||||
) -> tuple[int, str]:
|
||||
"""在独占同步会话内执行一次完整订阅新增事务。"""
|
||||
session = self._sync_session()
|
||||
try:
|
||||
outbox = SqlAlchemyOutboxRepository(session)
|
||||
command = CreateSubscriptionCommand(
|
||||
repository=SubscribeOper(session),
|
||||
unit_of_work=SqlAlchemyUnitOfWork(session),
|
||||
outbox=outbox,
|
||||
)
|
||||
|
||||
def delivered(subscribe_id: int) -> None:
|
||||
"""执行旧 post-commit 编排,全部成功后收口 durable intent。"""
|
||||
if after_commit:
|
||||
after_commit(subscribe_id)
|
||||
outbox.complete_by_event_key(
|
||||
subscription_added_event_key(subscribe_id, payload),
|
||||
datetime.now(timezone.utc),
|
||||
)
|
||||
if notification:
|
||||
outbox.complete_by_event_key(
|
||||
subscription_added_notification_key(subscribe_id, payload),
|
||||
datetime.now(timezone.utc),
|
||||
)
|
||||
outbox.complete_by_event_key(
|
||||
subscription_added_report_key(subscribe_id, payload),
|
||||
datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
return command.execute(
|
||||
identity,
|
||||
payload,
|
||||
username,
|
||||
delivered,
|
||||
notification,
|
||||
)
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
async def async_add(
|
||||
self,
|
||||
identity: dict,
|
||||
payload: dict,
|
||||
username: str | None = None,
|
||||
after_commit: AsyncAfterCommitEffect | None = None,
|
||||
notification: dict[str, object] | None = None,
|
||||
) -> tuple[int, str]:
|
||||
"""在独占异步会话作用域内执行一次完整订阅新增事务。"""
|
||||
async with self._async_session() as session:
|
||||
outbox = SqlAlchemyAsyncOutboxStager(session)
|
||||
command = AsyncCreateSubscriptionCommand(
|
||||
repository=SubscribeOper(session),
|
||||
unit_of_work=SqlAlchemyAsyncUnitOfWork(session),
|
||||
outbox=outbox,
|
||||
)
|
||||
|
||||
async def delivered(subscribe_id: int) -> None:
|
||||
"""异步执行旧编排,全部成功后收口 durable intent。"""
|
||||
if after_commit:
|
||||
await after_commit(subscribe_id)
|
||||
await outbox.complete_by_event_key(
|
||||
subscription_added_event_key(subscribe_id, payload),
|
||||
datetime.now(timezone.utc),
|
||||
)
|
||||
if notification:
|
||||
await outbox.complete_by_event_key(
|
||||
subscription_added_notification_key(subscribe_id, payload),
|
||||
datetime.now(timezone.utc),
|
||||
)
|
||||
await outbox.complete_by_event_key(
|
||||
subscription_added_report_key(subscribe_id, payload),
|
||||
datetime.now(timezone.utc),
|
||||
)
|
||||
|
||||
return await command.execute(
|
||||
identity,
|
||||
payload,
|
||||
username,
|
||||
delivered,
|
||||
notification,
|
||||
)
|
||||
@@ -0,0 +1,61 @@
|
||||
"""旧 Oper 写入口的 SQLAlchemy 事务执行适配器。"""
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from contextlib import AbstractAsyncContextManager
|
||||
from typing import TypeVar
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db.uow import SqlAlchemyAsyncUnitOfWork, SqlAlchemyUnitOfWork
|
||||
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
class TransactionalWriteRunner:
|
||||
"""为兼容写入口创建独占会话,并用 UoW 明确提交或回滚。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
sync_session: Callable[[], Session],
|
||||
async_session: Callable[[], AbstractAsyncContextManager[AsyncSession]],
|
||||
) -> None:
|
||||
"""保存同步会话工厂和异步会话上下文工厂。"""
|
||||
self._sync_session = sync_session
|
||||
self._async_session = async_session
|
||||
|
||||
def sync(self, operation: Callable[[Session], T]) -> T:
|
||||
"""在独占同步 Session 中执行操作并统一收口事务。"""
|
||||
session = self._sync_session()
|
||||
# 兼容 Oper 历史上会返回刚写入的 ORM 对象;提交后若过期,Session 关闭后连主键
|
||||
# 都无法读取。独占短会话没有后续一致性读取需求,因此保留已 flush 的字段快照。
|
||||
session.expire_on_commit = False
|
||||
unit_of_work = SqlAlchemyUnitOfWork(session)
|
||||
try:
|
||||
result = operation(session)
|
||||
unit_of_work.commit()
|
||||
return result
|
||||
except Exception:
|
||||
unit_of_work.rollback()
|
||||
raise
|
||||
finally:
|
||||
session.close()
|
||||
|
||||
async def async_(
|
||||
self,
|
||||
operation: Callable[[AsyncSession], Awaitable[T]],
|
||||
) -> T:
|
||||
"""在独占 AsyncSession 中执行操作并统一收口事务。"""
|
||||
async with self._async_session() as session:
|
||||
# 与同步兼容入口保持相同的返回对象生命周期。
|
||||
session.sync_session.expire_on_commit = False
|
||||
unit_of_work = SqlAlchemyAsyncUnitOfWork(session)
|
||||
try:
|
||||
result = await operation(session)
|
||||
await unit_of_work.commit()
|
||||
return result
|
||||
except Exception:
|
||||
await unit_of_work.rollback()
|
||||
raise
|
||||
@@ -0,0 +1,71 @@
|
||||
"""工作流执行状态事务适配器。"""
|
||||
|
||||
from collections.abc import Callable
|
||||
from typing import Any, TypeVar
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.application.workflow import WorkflowExecutionCommand
|
||||
from app.db.oper.workflow import WorkflowOper
|
||||
from app.db.uow import SqlAlchemyUnitOfWork
|
||||
|
||||
|
||||
_Result = TypeVar("_Result")
|
||||
|
||||
|
||||
class TransactionalWorkflowExecutionService:
|
||||
"""为每次工作流执行状态写入创建独立短会话和 UnitOfWork。"""
|
||||
|
||||
def __init__(self, session_factory: Callable[[], Session]) -> None:
|
||||
"""保存由启动组合根提供的同步 Session 工厂。"""
|
||||
self._session_factory = session_factory
|
||||
|
||||
def start(self, workflow_id: int) -> bool:
|
||||
"""以独立事务提交运行中状态。"""
|
||||
return self._run(lambda command: command.start(workflow_id))
|
||||
|
||||
def success(self, workflow_id: int, result: str | None = None) -> bool:
|
||||
"""以独立事务提交成功状态。"""
|
||||
return self._run(lambda command: command.success(workflow_id, result))
|
||||
|
||||
def fail(self, workflow_id: int, result: str) -> bool:
|
||||
"""以独立事务提交失败状态。"""
|
||||
return self._run(lambda command: command.fail(workflow_id, result))
|
||||
|
||||
def step(
|
||||
self,
|
||||
workflow_id: int,
|
||||
action_id: str,
|
||||
context: dict[str, Any],
|
||||
execution_state: dict[str, Any] | None = None,
|
||||
) -> bool:
|
||||
"""以独立事务提交动作进度。"""
|
||||
return self._run(
|
||||
lambda command: command.step(
|
||||
workflow_id,
|
||||
action_id,
|
||||
context,
|
||||
execution_state,
|
||||
)
|
||||
)
|
||||
|
||||
def reset(self, workflow_id: int, reset_count: bool = False) -> bool:
|
||||
"""以独立事务提交执行状态重置。"""
|
||||
return self._run(
|
||||
lambda command: command.reset(workflow_id, reset_count)
|
||||
)
|
||||
|
||||
def _run(
|
||||
self,
|
||||
operation: Callable[[WorkflowExecutionCommand], _Result],
|
||||
) -> _Result:
|
||||
"""创建短会话并把提交/回滚交给 Application command。"""
|
||||
session = self._session_factory()
|
||||
try:
|
||||
command = WorkflowExecutionCommand(
|
||||
repository=WorkflowOper(db=session),
|
||||
unit_of_work=SqlAlchemyUnitOfWork(session),
|
||||
)
|
||||
return operation(command)
|
||||
finally:
|
||||
session.close()
|
||||
+36
-15
@@ -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
@@ -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, ...],
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user