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