fix: 补齐 CI 质量门禁——mypy strict 类型标注 + ruff 行宽/导入排序

- watchlist_store/routers/watchlist: 裸 dict 补泛型参数 dict[str, object]
- routers/stream: event_gen 返回 AsyncGenerator[str, None] 注解
- app: _watch_symbols 返回类型;teardown 变量改名消除类型冲突
- ruff: 5 处超长行拆行、3 处导入排序自动修复

ruff / mypy (226 files) / 1078 tests 全绿
This commit is contained in:
GitHub
2026-09-01 17:52:36 +08:00
parent a79861af76
commit 6fe3f0f8c4
7 changed files with 33 additions and 20 deletions
+2 -1
View File
@@ -201,7 +201,8 @@ class GetSecurityQuotesCmd(BaseCommand[list[SecurityQuote]]):
raise TdxDecodeError(f"security_quotes 非法 market 值: {market_b}") from e
code = code_b.decode("utf-8").rstrip("\x00")
# 价格按品种有效小数位解析:股票/大盘指数 ÷100,ETF/基金/债券/880 统计指数 ÷1000Issue #8
# 价格按品种有效小数位解析:股票/大盘指数 ÷100
# ETF/基金/债券/880 统计指数 ÷1000Issue #8
divisor = 10 ** _price_decimal_digits(market, code)
p = price_raw / divisor
+4 -4
View File
@@ -88,7 +88,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
store = get_watchlist_store()
async def _watch_symbols():
async def _watch_symbols() -> list[tuple[Market, str]]:
# SQLite 存 "SH"/"SZ"/"BJ" 字符串,轮询器需要 Market 枚举
return [(Market[mkt], code) for mkt, code in store.symbols()]
@@ -132,10 +132,10 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
yield
# --- 关闭实时行情推送器 ---
streamer = getattr(app.state, "quote_streamer", None)
if streamer is not None:
streamer_svc = getattr(app.state, "quote_streamer", None)
if streamer_svc is not None:
try:
await streamer.stop()
await streamer_svc.stop()
except Exception:
logger.warning("QuoteStreamer stop failed", exc_info=True)
+2 -1
View File
@@ -19,7 +19,8 @@ import asyncio
import itertools
import logging
from collections.abc import Awaitable, Callable
from datetime import datetime, timedelta, timezone as dt_timezone
from datetime import datetime, timedelta
from datetime import timezone as dt_timezone
from typing import Any
import pandas as pd
+2 -1
View File
@@ -15,6 +15,7 @@ from __future__ import annotations
import asyncio
import json
import logging
from collections.abc import AsyncGenerator
from fastapi import APIRouter, Request
from fastapi.responses import StreamingResponse
@@ -37,7 +38,7 @@ async def stream_quotes(request: Request) -> StreamingResponse:
qid, queue = streamer.subscribe()
async def event_gen(): # type: ignore[no-untyped-def]
async def event_gen() -> AsyncGenerator[str, None]:
try:
# 首帧 hello:告诉前端连接可用 + 当前订阅规模
hello = {"type": "hello", "subscribers": streamer.subscriber_count}
+8 -6
View File
@@ -20,26 +20,28 @@ class WatchItemAdd(BaseModel):
class WatchlistResponse(BaseModel):
items: list[dict]
items: list[dict[str, object]]
count: int
@router.get("/watchlist", response_model=WatchlistResponse)
async def list_watchlist(group: str | None = Query(None, description="按分组过滤")) -> WatchlistResponse:
async def list_watchlist(
group: str | None = Query(None, description="按分组过滤"),
) -> WatchlistResponse:
"""列出全部自选(按加入顺序)。"""
items = get_watchlist_store().list_all(group=group)
return WatchlistResponse(items=[i.to_dict() for i in items], count=len(items))
@router.post("/watchlist", response_model=dict)
async def add_watch_item(req: WatchItemAdd) -> dict:
@router.post("/watchlist", response_model=dict[str, object])
async def add_watch_item(req: WatchItemAdd) -> dict[str, object]:
"""加入自选(幂等:重复加入仅刷新名称)。"""
item = get_watchlist_store().add(req.market, req.code, name=req.name, group=req.group)
return {"ok": True, "item": item.to_dict()}
@router.delete("/watchlist/{market}/{code}", response_model=dict)
async def remove_watch_item(market: str, code: str) -> dict:
@router.delete("/watchlist/{market}/{code}", response_model=dict[str, object])
async def remove_watch_item(market: str, code: str) -> dict[str, object]:
"""移除自选。"""
if market.upper() not in {"SZ", "SH", "BJ"}:
raise HTTPException(status_code=400, detail=f"非法市场: {market}")
+13 -5
View File
@@ -56,7 +56,7 @@ class WatchItem:
"""前端统一标识:SH600000 形式。"""
return f"{self.market}{self.code}"
def to_dict(self) -> dict:
def to_dict(self) -> dict[str, object]:
return {
"market": self.market,
"code": self.code,
@@ -125,7 +125,8 @@ class WatchlistStore:
market = market.upper()
with _write_lock, self._connect() as conn:
row = conn.execute(
"SELECT * FROM watchlist WHERE market = ? AND code = ?", (market, code)
"SELECT * FROM watchlist WHERE market = ? AND code = ?",
(market, code),
).fetchone()
if row is not None:
if name and name != row["name"]:
@@ -141,19 +142,26 @@ class WatchlistStore:
created_at=row["created_at"],
sort_order=row["sort_order"],
)
next_order = conn.execute("SELECT COALESCE(MAX(sort_order), 0) + 1 FROM watchlist").fetchone()[0]
next_order = conn.execute(
"SELECT COALESCE(MAX(sort_order), 0) + 1 FROM watchlist"
).fetchone()[0]
conn.execute(
"INSERT INTO watchlist (market, code, name, group_name, created_at, sort_order)"
" VALUES (?, ?, ?, ?, ?, ?)",
(market, code, name, group, _now_iso(), next_order),
)
return WatchItem(market=market, code=code, name=name, group_name=group, created_at=_now_iso(), sort_order=next_order)
return WatchItem(
market=market, code=code, name=name, group_name=group,
created_at=_now_iso(), sort_order=next_order,
)
def remove(self, market: str, code: str) -> bool:
"""移除自选;返回是否确实删除了一条。"""
market = market.upper()
with _write_lock, self._connect() as conn:
cur = conn.execute("DELETE FROM watchlist WHERE market = ? AND code = ?", (market, code))
cur = conn.execute(
"DELETE FROM watchlist WHERE market = ? AND code = ?", (market, code)
)
return cur.rowcount > 0
+2 -2
View File
@@ -12,7 +12,6 @@ from easy_tdx.models.enums import Market
from easy_tdx.web.quote_streamer import INDEX_SYMBOLS, QuoteStreamer, _is_trading_hours
from easy_tdx.web.watchlist_store import WatchlistStore
# ── WatchlistStore ──────────────────────────────────────────────────────────
@@ -147,7 +146,8 @@ def test_streamer_backpressure_drops_oldest() -> None:
def test_is_trading_hours() -> None:
from datetime import datetime, timedelta, timezone as dt_timezone
from datetime import datetime, timedelta
from datetime import timezone as dt_timezone
tz = dt_timezone(timedelta(hours=8))
assert _is_trading_hours(datetime(2026, 9, 1, 10, 0, tzinfo=tz)) is True # 周二盘中