mirror of
https://ghfast.top/https://github.com/aeroxw/easy_tdx_max.git
synced 2026-09-12 16:54:20 +08:00
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:
@@ -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 统计指数 ÷1000(Issue #8)
|
||||
# 价格按品种有效小数位解析:股票/大盘指数 ÷100;
|
||||
# ETF/基金/债券/880 统计指数 ÷1000(Issue #8)
|
||||
divisor = 10 ** _price_decimal_digits(market, code)
|
||||
p = price_raw / divisor
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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 # 周二盘中
|
||||
|
||||
Reference in New Issue
Block a user