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 raise TdxDecodeError(f"security_quotes 非法 market 值: {market_b}") from e
code = code_b.decode("utf-8").rstrip("\x00") 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) divisor = 10 ** _price_decimal_digits(market, code)
p = price_raw / divisor p = price_raw / divisor
+4 -4
View File
@@ -88,7 +88,7 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
store = get_watchlist_store() store = get_watchlist_store()
async def _watch_symbols(): async def _watch_symbols() -> list[tuple[Market, str]]:
# SQLite 存 "SH"/"SZ"/"BJ" 字符串,轮询器需要 Market 枚举 # SQLite 存 "SH"/"SZ"/"BJ" 字符串,轮询器需要 Market 枚举
return [(Market[mkt], code) for mkt, code in store.symbols()] return [(Market[mkt], code) for mkt, code in store.symbols()]
@@ -132,10 +132,10 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
yield yield
# --- 关闭实时行情推送器 --- # --- 关闭实时行情推送器 ---
streamer = getattr(app.state, "quote_streamer", None) streamer_svc = getattr(app.state, "quote_streamer", None)
if streamer is not None: if streamer_svc is not None:
try: try:
await streamer.stop() await streamer_svc.stop()
except Exception: except Exception:
logger.warning("QuoteStreamer stop failed", exc_info=True) logger.warning("QuoteStreamer stop failed", exc_info=True)
+2 -1
View File
@@ -19,7 +19,8 @@ import asyncio
import itertools import itertools
import logging import logging
from collections.abc import Awaitable, Callable 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 from typing import Any
import pandas as pd import pandas as pd
+2 -1
View File
@@ -15,6 +15,7 @@ from __future__ import annotations
import asyncio import asyncio
import json import json
import logging import logging
from collections.abc import AsyncGenerator
from fastapi import APIRouter, Request from fastapi import APIRouter, Request
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
@@ -37,7 +38,7 @@ async def stream_quotes(request: Request) -> StreamingResponse:
qid, queue = streamer.subscribe() qid, queue = streamer.subscribe()
async def event_gen(): # type: ignore[no-untyped-def] async def event_gen() -> AsyncGenerator[str, None]:
try: try:
# 首帧 hello:告诉前端连接可用 + 当前订阅规模 # 首帧 hello:告诉前端连接可用 + 当前订阅规模
hello = {"type": "hello", "subscribers": streamer.subscriber_count} hello = {"type": "hello", "subscribers": streamer.subscriber_count}
+8 -6
View File
@@ -20,26 +20,28 @@ class WatchItemAdd(BaseModel):
class WatchlistResponse(BaseModel): class WatchlistResponse(BaseModel):
items: list[dict] items: list[dict[str, object]]
count: int count: int
@router.get("/watchlist", response_model=WatchlistResponse) @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) items = get_watchlist_store().list_all(group=group)
return WatchlistResponse(items=[i.to_dict() for i in items], count=len(items)) return WatchlistResponse(items=[i.to_dict() for i in items], count=len(items))
@router.post("/watchlist", response_model=dict) @router.post("/watchlist", response_model=dict[str, object])
async def add_watch_item(req: WatchItemAdd) -> dict: 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) item = get_watchlist_store().add(req.market, req.code, name=req.name, group=req.group)
return {"ok": True, "item": item.to_dict()} return {"ok": True, "item": item.to_dict()}
@router.delete("/watchlist/{market}/{code}", response_model=dict) @router.delete("/watchlist/{market}/{code}", response_model=dict[str, object])
async def remove_watch_item(market: str, code: str) -> dict: async def remove_watch_item(market: str, code: str) -> dict[str, object]:
"""移除自选。""" """移除自选。"""
if market.upper() not in {"SZ", "SH", "BJ"}: if market.upper() not in {"SZ", "SH", "BJ"}:
raise HTTPException(status_code=400, detail=f"非法市场: {market}") raise HTTPException(status_code=400, detail=f"非法市场: {market}")
+13 -5
View File
@@ -56,7 +56,7 @@ class WatchItem:
"""前端统一标识:SH600000 形式。""" """前端统一标识:SH600000 形式。"""
return f"{self.market}{self.code}" return f"{self.market}{self.code}"
def to_dict(self) -> dict: def to_dict(self) -> dict[str, object]:
return { return {
"market": self.market, "market": self.market,
"code": self.code, "code": self.code,
@@ -125,7 +125,8 @@ class WatchlistStore:
market = market.upper() market = market.upper()
with _write_lock, self._connect() as conn: with _write_lock, self._connect() as conn:
row = conn.execute( row = conn.execute(
"SELECT * FROM watchlist WHERE market = ? AND code = ?", (market, code) "SELECT * FROM watchlist WHERE market = ? AND code = ?",
(market, code),
).fetchone() ).fetchone()
if row is not None: if row is not None:
if name and name != row["name"]: if name and name != row["name"]:
@@ -141,19 +142,26 @@ class WatchlistStore:
created_at=row["created_at"], created_at=row["created_at"],
sort_order=row["sort_order"], 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( conn.execute(
"INSERT INTO watchlist (market, code, name, group_name, created_at, sort_order)" "INSERT INTO watchlist (market, code, name, group_name, created_at, sort_order)"
" VALUES (?, ?, ?, ?, ?, ?)", " VALUES (?, ?, ?, ?, ?, ?)",
(market, code, name, group, _now_iso(), next_order), (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: def remove(self, market: str, code: str) -> bool:
"""移除自选;返回是否确实删除了一条。""" """移除自选;返回是否确实删除了一条。"""
market = market.upper() market = market.upper()
with _write_lock, self._connect() as conn: 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 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.quote_streamer import INDEX_SYMBOLS, QuoteStreamer, _is_trading_hours
from easy_tdx.web.watchlist_store import WatchlistStore from easy_tdx.web.watchlist_store import WatchlistStore
# ── WatchlistStore ────────────────────────────────────────────────────────── # ── WatchlistStore ──────────────────────────────────────────────────────────
@@ -147,7 +146,8 @@ def test_streamer_backpressure_drops_oldest() -> None:
def test_is_trading_hours() -> 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)) tz = dt_timezone(timedelta(hours=8))
assert _is_trading_hours(datetime(2026, 9, 1, 10, 0, tzinfo=tz)) is True # 周二盘中 assert _is_trading_hours(datetime(2026, 9, 1, 10, 0, tzinfo=tz)) is True # 周二盘中