refactor(types): declare download history and file query results

This commit is contained in:
jxxghp
2026-10-16 20:24:33 +08:00
parent 38cec1537f
commit 751e223dcc
5 changed files with 47 additions and 33 deletions

View File

@@ -1,16 +1,16 @@
import time
from typing import Any, List, Optional
from typing import Any, List, Optional, Self
from sqlalchemy import JSON, Index, Integer, String, delete, func, select, update
from sqlalchemy import JSON, ColumnElement, Index, Integer, String, delete, func, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import Mapped, Session, mapped_column
from sqlalchemy.orm import InstrumentedAttribute, Mapped, Session, mapped_column
from app.db.base import Base, execute_dml, get_id_column
from app.db.models._constraints import media_identity_constraint
from app.schemas.types import MediaSource
def _title_like(column, title: str):
def _title_like(column: InstrumentedAttribute[str], title: str) -> ColumnElement[bool]:
"""构造跨数据库大小写不敏感的标题匹配条件。"""
return column.ilike(f"%{title}%")
@@ -84,15 +84,17 @@ class DownloadHistory(Base):
)
@classmethod
def get_by_hash(cls, db: Session, download_hash: str):
return db.execute(
def get_by_hash(cls, db: Session, download_hash: str) -> Optional["DownloadHistory"]:
"""按下载任务哈希读取登记时间最新的下载历史。"""
history: Optional[DownloadHistory] = db.execute(
select(DownloadHistory)
.where(DownloadHistory.download_hash == download_hash)
.order_by(DownloadHistory.date.desc())
).scalars().first()
return history
@classmethod
def get_by_hashes(cls, db: Session, download_hashes: List[str]):
def get_by_hashes(cls, db: Session, download_hashes: List[str]) -> list["DownloadHistory"]:
"""
批量查询多个下载任务的最新历史记录,避免在上层形成 N+1 查询。
"""
@@ -127,7 +129,7 @@ class DownloadHistory(Base):
def get_by_media_identity(
cls, db: Session, media_source: MediaSource, media_id: str,
music_type: Optional[str] = None,
):
) -> list["DownloadHistory"]:
"""按规范媒体身份查询下载历史。"""
if not media_source or media_id is None or not str(media_id).strip():
return []
@@ -142,7 +144,8 @@ class DownloadHistory(Base):
@classmethod
def list_by_page(
cls, db: Session, page: int = 1, count: int = 30
):
) -> list["DownloadHistory"]:
"""按时间和主键倒序分页读取下载历史。"""
return list(db.execute(
select(DownloadHistory)
.order_by(DownloadHistory.date.desc(), DownloadHistory.id.desc())
@@ -153,7 +156,8 @@ class DownloadHistory(Base):
@classmethod
async def async_list_by_page(
cls, db: AsyncSession, page: int = 1, count: int = 30
):
) -> list[Self]:
"""异步按时间和主键倒序分页读取下载历史。"""
result = await db.execute(
select(cls)
.order_by(cls.date.desc(), cls.id.desc())
@@ -169,7 +173,8 @@ class DownloadHistory(Base):
title: str,
page: int = 1,
count: int = 30,
):
) -> list[Self]:
"""异步按标题模糊匹配并分页读取下载历史。"""
query = (
select(cls).filter(_title_like(cls.title, title)).order_by(cls.date.desc())
)
@@ -178,24 +183,29 @@ class DownloadHistory(Base):
return list(result.scalars().all())
@classmethod
async def async_count(cls, db: AsyncSession):
async def async_count(cls, db: AsyncSession) -> int:
"""异步统计下载历史总数,空表返回零。"""
result = await db.execute(select(func.count(cls.id)))
return result.scalar()
count: int = result.scalar()
return count
@classmethod
async def async_count_by_title(cls, db: AsyncSession, title: str):
async def async_count_by_title(cls, db: AsyncSession, title: str) -> int:
"""异步统计标题模糊匹配的下载历史数量。"""
result = await db.execute(
select(func.count(cls.id)).filter(_title_like(cls.title, title))
)
return result.scalar()
count: int = result.scalar()
return count
@classmethod
def get_by_path(cls, db: Session, path: str):
def get_by_path(cls, db: Session, path: str) -> Optional["DownloadHistory"]:
"""保存目录被多次下载复用时,按登记顺序返回最新历史。"""
return db.execute(
history: Optional[DownloadHistory] = db.execute(
select(DownloadHistory).where(DownloadHistory.path == path)
.order_by(DownloadHistory.id.desc()).limit(1)
).scalars().first()
return history
@classmethod
def get_last_by(
@@ -208,7 +218,7 @@ class DownloadHistory(Base):
episode: Optional[str] = None,
media_source: Optional[MediaSource] = None,
media_id: Optional[str] = None,
):
) -> list["DownloadHistory"]:
"""
按媒体身份、季集或标题年份查询下载记录。
"""
@@ -235,7 +245,7 @@ class DownloadHistory(Base):
).scalars().all())
@classmethod
def list_by_user_date(cls, db: Session, date: str, username: Optional[str] = None):
def list_by_user_date(cls, db: Session, date: str, username: Optional[str] = None) -> list["DownloadHistory"]:
"""
查询某用户某时间之前的下载历史。
@@ -261,7 +271,7 @@ class DownloadHistory(Base):
media_source: MediaSource,
media_id: str,
seasons: Optional[str] = None,
):
) -> list["DownloadHistory"]:
"""
查询某时间之后的下载历史
"""
@@ -278,7 +288,8 @@ class DownloadHistory(Base):
).scalars().all())
@classmethod
def list_by_type(cls, db: Session, mtype: str, days: int):
def list_by_type(cls, db: Session, mtype: str, days: int) -> list["DownloadHistory"]:
"""读取指定媒体类型在最近若干天内的下载历史。"""
return list(db.execute(
select(DownloadHistory).where(
DownloadHistory.type == mtype,
@@ -340,25 +351,30 @@ class DownloadFiles(Base):
)
@classmethod
def get_by_hash(cls, db: Session, download_hash: str, state: Optional[int] = None):
def get_by_hash(cls, db: Session, download_hash: str, state: Optional[int] = None) -> list[Self]:
"""按下载任务哈希读取文件列表,可选限定文件状态。"""
statement = select(cls).where(cls.download_hash == download_hash)
if state is not None:
statement = statement.where(cls.state == state)
return list(db.execute(statement).scalars().all())
@classmethod
def get_by_fullpath(cls, db: Session, fullpath: str, all_files: bool = False):
def get_by_fullpath(cls, db: Session, fullpath: str, all_files: bool = False) -> list[Self] | Self | None:
"""按完整路径返回最新记录,或在 all_files 为真时返回全部记录。"""
result = db.execute(
select(cls).where(cls.fullpath == fullpath).order_by(cls.id.desc())
).scalars()
return list(result.all()) if all_files else result.first()
files: list[Self] | Self | None = list(result.all()) if all_files else result.first()
return files
@classmethod
def get_by_savepath(cls, db: Session, savepath: str):
def get_by_savepath(cls, db: Session, savepath: str) -> list[Self]:
"""读取同一保存目录下的全部下载文件记录。"""
return list(db.execute(select(cls).where(cls.savepath == savepath)).scalars().all())
@classmethod
def delete_by_fullpath(cls, db: Session, fullpath: str):
def delete_by_fullpath(cls, db: Session, fullpath: str) -> None:
"""把完整路径匹配的正常文件标记为删除,保留历史记录。"""
db.execute(
update(cls).where(cls.fullpath == fullpath, cls.state == 1).values(state=0)
)

View File

@@ -75,7 +75,7 @@ uv run --locked --no-sync python scripts/architecture/complexity.py
回调和返回值类型;全仓错误计数为零并不保证单独纳入 strict frontier 后也为零。
2026-10-16 开始逐文件清理类型债务,`app/domain/title.py` 通过局部可空类型声明保留
既有搜索解析行为,并纳入严格清单;当前清单共 131 个文件(130 个生产文件和 1 个脚本)。
既有搜索解析行为,并纳入严格清单;当前清单共 132 个文件(131 个生产文件和 1 个脚本)。
每个治理提交聚焦一个生产文件及其必要测试、门禁配置、真实下降基线和指标文档。
验收同时检查完整严格清单与全宿主 Linux 分析,避免 `follow_imports = skip` 隐藏依赖
签名冲突;不通过忽略诊断或放宽类型合同接受回退。最新文件清单以 `mypy.ini` 为准,

View File

@@ -106,9 +106,9 @@ ARCH-201 至 ARCH-204 均达到实现、验证、提交、推送和远端门禁
| Model/Oper 事务债务 | 0 | 自建 Session、自动事务装饰器、直接 commit/rollback 等基线均为零 |
| Module Contract | 215 specs / 211 methods / 263 calls | 动态方法名为 0;分类事实富化通过显式合同进入插件调度,旧 YAML 分类方法已移除;download 新增 custom_words 可选参数,旧 provider 签名通过显式合同兼容 |
| Event Contract | 53 | 均已有 payload model,但当前全部是 diagnostic enforcement |
| Python 源码量 | 372,622 行 | 本次重新扫描物理行数,排除 `app/plugins/**`;74 个文件超过 1,000 行,12 个超过 2,000 行;源码尺寸仅作观察 |
| Python 源码量 | 372,638 行 | 本次重新扫描物理行数,排除 `app/plugins/**`;74 个文件超过 1,000 行,12 个超过 2,000 行;源码尺寸仅作观察 |
| 长方法 | 309 个超过 80 行 | 本次 AST 扫描排除 `app/plugins/**`;61 个超过 150 行,17 个超过 250 行;新增函数仍受 C901/嵌套深度门禁约束 |
| 全量 mypy 历史债务 | 8,648 / 424 文件 | 通知排版补齐 Slack 消息块类型,历史债务减少 2 项;工作流资源过滤改用领域对象调用过滤链,恢复的节点输出在消费前归一化,并收紧可空资源列表的类型低水位;消息媒体交互类型债务已清零,canonical Facade 与 endpoint 类型边界已补齐,低水位只允许继续下降;微信 ClawBot 入站 item_list 媒体解析改为按子项键名分派后,类型错误减少 4 项;站点解析器分享率改用浮点初值,类型错误减少 20 项;本次下载与文件安全修复再减少 3 项;Transmission 进度回退值归一化再减少 2 项,逐文件逐错误码无增长 |
| 全量 mypy 历史债务 | 8,627 / 423 文件 | 通知排版补齐 Slack 消息块类型,历史债务减少 2 项;工作流资源过滤改用领域对象调用过滤链,恢复的节点输出在消费前归一化,并收紧可空资源列表的类型低水位;消息媒体交互类型债务已清零,canonical Facade 与 endpoint 类型边界已补齐,低水位只允许继续下降;微信 ClawBot 入站 item_list 媒体解析改为按子项键名分派后,类型错误减少 4 项;站点解析器分享率改用浮点初值,类型错误减少 20 项;本次下载与文件安全修复再减少 3 项;Transmission 进度回退值归一化再减少 2 项,逐文件逐错误码无增长 |
| Ruff 历史诊断 | 98 | 低水位门禁通过,但规则集只覆盖 `E4/E7/E9/F/I` |
| 覆盖率固定基线 | Application 80.00%,Domain 80.00% | Chain、Runtime、Agent、Adapter、Startup 未进入包级覆盖率门禁 |

View File

@@ -110,6 +110,7 @@ files =
app/db/models/agenttask.py,
app/db/models/agenttaskrun.py,
app/db/models/downloadfailure.py,
app/db/models/downloadhistory.py,
app/db/models/mediaserver.py,
app/db/models/message.py,
app/db/models/passkey.py,

View File

@@ -977,9 +977,6 @@
"no-any-return": 1,
"redundant-cast": 2
},
"app/db/models/downloadhistory.py": {
"no-untyped-def": 19
},
"app/db/models/subscribe.py": {
"arg-type": 8,
"no-untyped-def": 18
@@ -990,7 +987,7 @@
"type-arg": 1
},
"app/db/oper/downloadhistory.py": {
"no-any-return": 3,
"no-any-return": 1,
"no-untyped-def": 9,
"type-arg": 3,
"union-attr": 2