mirror of
https://ghfast.top/https://github.com/aeroxw/easy-tdx.git
synced 2026-09-12 14:34:15 +08:00
feat(web): /bars 迁移到 MacClient + 支持复权(issue #43)
Web 获取 K 线此前用 AsyncTdxClient.get_security_bars(标准协议不支持复权),
导致 REST API 无法取前复权/后复权数据。将 /bars(个股 K 线)迁移到
AsyncMacClient.get_stock_kline(MAC 协议,支持 NONE/QFQ/HFQ + QFQ 负价兜底),
保持旧输出契约不变(日线 date 列、分钟线 datetime 列、OHLC 顺序、无 float_shares),
新增 adjust 参数(默认 QFQ),MAC 主机不可用时自动回退标准 TdxClient。
⚠️ 半破坏性变更:/bars 默认复权从"不复权"改为 QFQ(前复权)。老调用方若需
不复权请显式传 ?adjust=NONE。输出 DataFrame 列名/顺序/字段与旧版完全一致
(_normalize_mac_df 规整),仅价格数值因复权变化。
改动:
- bars.py:/bars 改走 mac_client.get_stock_kline(adjust=...),MAC 不可用回退
get_security_bars(无复权 + warning);新增 _normalize_mac_df 规整输出契约;
新增 adjust 查询参数(默认 QFQ)。
- convert.py:period_times_from_category(KlineCategory→Period 映射,显式处理
YEAR 9→YEARLY 11、SEASON→QUARTERLY)+ adjust_from_str。
- deps.py:get_mac_client_optional(未连接返回 None,供回退判断)。
- schemas.py:AdjustEnum(OpenAPI 文档用)。
测试:新增 7 个(period_times 映射全表 + 不可映射值、adjust 转换、_normalize_mac_df
日线/分钟线/空 df)。全套 996 passed;ruff format/check + mypy 改动文件零错误。
不在本次范围:/bars/index(指数 K 线,MAC 另一套接口)、/minute、/transaction*
(MacClient tick_chart 语义不同,暂不迁移)。
This commit is contained in:
@@ -2,6 +2,32 @@
|
||||
|
||||
本文件记录 easy-tdx 的版本变更。格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/)。
|
||||
|
||||
## [1.20.6] — 2026-08-05
|
||||
|
||||
**Web `/bars` 端点迁移到 MacClient + 支持复权**(Issue #43)—— 用户反馈 Web 获取 K 线用的还是标准 TdxClient(标准协议本身不支持复权),导致 REST API 无法取前复权/后复权数据。本次将 `/bars`(个股 K 线)迁移到 `AsyncMacClient.get_stock_kline`(MAC 协议,支持 NONE/QFQ/HFQ + QFQ 负价兜底),**保持旧输出契约不变**(日线 `date` 列、分钟线 `datetime` 列、OHLC 顺序、无 `float_shares`),新增 `adjust` 参数(默认 QFQ),MAC 主机不可用时自动回退标准 TdxClient。
|
||||
|
||||
### ⚠️ 半破坏性变更
|
||||
|
||||
- **`/bars` 默认复权方式从"不复权"改为 QFQ(前复权)**。此前 `/bars` 透传 `AsyncTdxClient.get_security_bars`(无复权参数),默认返回原始价格。迁移后默认 `adjust=QFQ`,符合大多数看盘/回测场景。**老调用方若需不复权,请显式传 `?adjust=NONE`**。输出 DataFrame 的列名/顺序/字段与旧版完全一致(已规整),仅价格数值因复权变化。
|
||||
|
||||
### 新增
|
||||
|
||||
- **`/bars` 支持复权**(`src/easy_tdx/web/routers/bars.py`)—— 优先走 `AsyncMacClient.get_stock_kline(adjust=...)`(支持 NONE/QFQ/HFQ,QFQ 对深层历史负价有本地重算兜底);MAC 主机未连接时自动回退 `AsyncTdxClient.get_security_bars`(无复权,adjust 参数忽略并 warning)。新增查询参数 `adjust`(默认 QFQ)。
|
||||
- **`_normalize_mac_df`**(`src/easy_tdx/web/routers/bars.py`)—— 规整 MacClient 输出以匹配旧 `/bars` 契约:日线及以上 `datetime`→`date`(截断时分秒)、drop `float_shares`、OHLC 列顺序对齐 `open/close/high/low`。迁移后调用方输出契约零变化。
|
||||
- **`period_times_from_category`**(`src/easy_tdx/web/convert.py`)—— 标准 `KlineCategory` → MAC `(Period, times)` 映射查表(显式处理 YEAR 9→YEARLY 11、SEASON→QUARTERLY 值/名差异)。
|
||||
- **`adjust_from_str`**(`src/easy_tdx/web/convert.py`)—— 字符串 → `Adjust` 枚举(NONE/QFQ/HFQ,支持大小写和数字字符串)。
|
||||
- **`get_mac_client_optional`**(`src/easy_tdx/web/deps.py`)—— MAC client 依赖注入的可选版(未连接返回 None 而非抛 503),供 `/bars` 回退判断;原 `get_mac_client`(强制版)不动,其他 `/mac/*` 端点继续用。
|
||||
- **`AdjustEnum`**(`src/easy_tdx/web/schemas.py`)—— OpenAPI 文档展示用。
|
||||
|
||||
### 测试
|
||||
|
||||
- 新增 7 个测试(`tests/unit/test_web_api.py`):`period_times_from_category` 完整映射(10 个 KlineCategory,重点 YEAR/SEASON)+ 不可映射值抛错;`adjust_from_str` 名称/大小写/数字/非法值;`_normalize_mac_df` 日线(datetime→date)/分钟线(保留 datetime)/空 df 三场景。全套 27 web 测试通过。
|
||||
|
||||
### 不在本次范围
|
||||
|
||||
- `/bars/index`(指数 K 线):MAC 指数 K 线是另一套接口,需单独评估。
|
||||
- `/minute`、`/transaction*`(分时/逐笔):MacClient 的 `get_tick_chart` 语义与标准分时不同,暂不迁移。
|
||||
|
||||
## [1.20.5] — 2026-08-05
|
||||
|
||||
**资金流空数据故障转移**(Issue #41)—— 用户反馈 `get_history_fund_flow(SH, "600519")` 返回空 DataFrame,日志显示"K线响应为空(声称 800 条但首条即解析失败...)"。排查定位:当前 host 对常见标的也返回 `ret_count` 撒谎的空 body,但资金流这条兼容回退路径(直连空 → 拉 K 线 + 历史逐笔重算)**未接入 v1.20.4 的空数据故障转移**,"服务器回包正常但内容是假的空"既非 `TdxConnectionError` 也不触发换台,用户卡在坏服务器上拿不到数据。本次将资金流路径接入与 K 线同源的空数据故障转移。
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
||||
|
||||
[project]
|
||||
name = "easy-tdx"
|
||||
version = "1.20.5"
|
||||
version = "1.20.6"
|
||||
description = "通达信 TCP 协议行情数据客户端,支持在线行情、离线数据读取与写入同步"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.10"
|
||||
|
||||
@@ -102,6 +102,68 @@ def sort_order_from_str(s: str) -> Any:
|
||||
raise ValueError(f"无效排序方向 '{s}',可选值: {valid}") from None
|
||||
|
||||
|
||||
def period_times_from_category(category: Any) -> tuple[Any, int]:
|
||||
"""将标准 KlineCategory 映射到 MAC 的 (Period, times)。
|
||||
|
||||
标准协议的 KlineCategory 与 MAC 协议的 Period 枚举值并非一一对应
|
||||
(如 YEAR=9 而 YEARLY=11),且 MAC 用 Period+times 表达多周期。这里做
|
||||
显式查表,times 一律为 1(KlineCategory 无多周期概念)。
|
||||
|
||||
Args:
|
||||
category: ``KlineCategory`` 枚举值(或其 int)。
|
||||
|
||||
Returns:
|
||||
``(Period, times)`` 元组。
|
||||
|
||||
Raises:
|
||||
ValueError: 无法映射的 KlineCategory 值。
|
||||
"""
|
||||
from easy_tdx.mac.enums import Period
|
||||
|
||||
# 显式查表:标准 KlineCategory 值 → (Period, times)
|
||||
# 注意 YEAR(9)→YEARLY(11)、SEASON(10)→QUARTERLY(10) 值/名不同
|
||||
mapping = {
|
||||
0: (Period.MIN_5, 1), # KlineCategory.MIN_5
|
||||
1: (Period.MIN_15, 1), # KlineCategory.MIN_15
|
||||
2: (Period.MIN_30, 1), # KlineCategory.MIN_30
|
||||
3: (Period.MIN_60, 1), # KlineCategory.MIN_60
|
||||
4: (Period.DAILY, 1), # KlineCategory.DAY
|
||||
5: (Period.WEEKLY, 1), # KlineCategory.WEEK
|
||||
6: (Period.MONTHLY, 1), # KlineCategory.MONTH
|
||||
7: (Period.MIN_1, 1), # KlineCategory.MIN_1
|
||||
9: (Period.YEARLY, 1), # KlineCategory.YEAR(值 9→Period.YEARLY 值 11)
|
||||
10: (Period.QUARTERLY, 1), # KlineCategory.SEASON → Period.QUARTERLY
|
||||
}
|
||||
key = int(category)
|
||||
if key not in mapping:
|
||||
valid = "MIN_1/5/15/30/60, DAY, WEEK, MONTH, SEASON, YEAR"
|
||||
raise ValueError(f"无法映射的 K线周期值 {key},/bars 仅支持 {valid}")
|
||||
return mapping[key]
|
||||
|
||||
|
||||
def adjust_from_str(s: str) -> Any:
|
||||
"""将字符串转为 Adjust 枚举(NONE/QFQ/HFQ),支持大小写和数字字符串。
|
||||
|
||||
>>> adjust_from_str("QFQ") # 前复权
|
||||
>>> adjust_from_str("qfq") # 也正常(自动转大写)
|
||||
>>> adjust_from_str("1") # 数字字符串也行(= QFQ)
|
||||
>>> adjust_from_str("XXX") # ValueError
|
||||
"""
|
||||
from easy_tdx.mac.enums import Adjust
|
||||
|
||||
key = s.upper()
|
||||
try:
|
||||
return Adjust[key]
|
||||
except KeyError:
|
||||
pass
|
||||
try:
|
||||
return Adjust(int(key))
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
valid = ", ".join(a.name for a in Adjust)
|
||||
raise ValueError(f"无效复权类型 '{s}',可选值: {valid}") from None
|
||||
|
||||
|
||||
def category_mac_from_str(s: str) -> Any:
|
||||
"""将字符串转为 MAC Category 枚举(A/SH/SZ/KCB/BJ/CYB/...)。"""
|
||||
from easy_tdx.mac.enums import Category
|
||||
|
||||
@@ -25,6 +25,15 @@ def get_mac_client(request: Request) -> Any:
|
||||
return client
|
||||
|
||||
|
||||
def get_mac_client_optional(request: Request) -> Any | None:
|
||||
"""从 app.state 获取 AsyncMacClient 实例,未连接时返回 None(不抛异常)。
|
||||
|
||||
供需要"MAC 不可用时自动回退标准 TdxClient"的端点使用(如 ``/bars``)。
|
||||
其他强制依赖 MAC 的端点(``/mac/*``)仍用 :func:`get_mac_client`。
|
||||
"""
|
||||
return request.app.state.mac_client
|
||||
|
||||
|
||||
def get_ex_client(request: Request) -> Any:
|
||||
"""从 app.state 获取共享的 AsyncExTdxClient 实例(可选)。"""
|
||||
client: Any | None = request.app.state.ex_client
|
||||
|
||||
@@ -2,21 +2,65 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
import pandas as pd
|
||||
from fastapi import APIRouter, Depends, Query
|
||||
|
||||
from easy_tdx.web.convert import category_from_str, market_from_str
|
||||
from easy_tdx.web.deps import get_client
|
||||
from easy_tdx.models.enums import KlineCategory
|
||||
from easy_tdx.web.convert import (
|
||||
adjust_from_str,
|
||||
category_from_str,
|
||||
market_from_str,
|
||||
market_value_from_str,
|
||||
period_times_from_category,
|
||||
)
|
||||
from easy_tdx.web.deps import get_client, get_mac_client_optional
|
||||
from easy_tdx.web.schemas import DataFrameResponse
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
router = APIRouter(tags=["bars"])
|
||||
|
||||
# 规整后保持的列顺序(匹配旧 SecurityBar 输出契约)
|
||||
_NORMAL_COLS = ["open", "close", "high", "low", "vol", "amount"]
|
||||
|
||||
|
||||
def _df_resp(df: Any) -> DataFrameResponse:
|
||||
return DataFrameResponse.from_dataframe(df)
|
||||
|
||||
|
||||
def _normalize_mac_df(df: pd.DataFrame, daily_plus: bool) -> pd.DataFrame:
|
||||
"""规整 MacClient.get_stock_kline 的输出以匹配旧 /bars 契约。
|
||||
|
||||
MacClient 返回 ``datetime`` 列(含时分秒)+ ``float_shares`` 列,OHLC 顺序为
|
||||
open/high/low/close。旧 /bars(SecurityBar 路径)日线返回 ``date`` 列(仅日期)、
|
||||
分钟线返回 ``datetime`` 列,无 float_shares,OHLC 顺序为 open/close/high/low。
|
||||
本函数做对齐,保证迁移后调用方输出契约不变。
|
||||
|
||||
Args:
|
||||
df: MacClient 返回的 DataFrame(可能为空)。
|
||||
daily_plus: True=日线及以上周期(datetime→date),False=分钟线(保留 datetime)。
|
||||
"""
|
||||
if df.empty:
|
||||
return df
|
||||
out = df.copy()
|
||||
if "float_shares" in out.columns:
|
||||
out = out.drop(columns=["float_shares"])
|
||||
time_col = "date" if daily_plus else "datetime"
|
||||
if "datetime" in out.columns:
|
||||
if daily_plus:
|
||||
# 截断为仅日期(00:00:00),与旧 _merge_bar_datetime 的 date 列语义一致
|
||||
out["datetime"] = pd.to_datetime(out["datetime"]).dt.normalize()
|
||||
out = out.rename(columns={"datetime": time_col})
|
||||
# 重排列顺序:时间列在前,OHLC 顺序 open/close/high/low,再 vol/amount
|
||||
cols = [c for c in [time_col, *_NORMAL_COLS] if c in out.columns]
|
||||
# 兜底:保留未列出的列(理论上不应有),追加到末尾
|
||||
cols += [c for c in out.columns if c not in cols]
|
||||
return out[cols]
|
||||
|
||||
|
||||
@router.get("/bars", response_model=DataFrameResponse)
|
||||
async def security_bars(
|
||||
market: str = Query(..., description="市场: SZ, SH, BJ"),
|
||||
@@ -30,11 +74,41 @@ async def security_bars(
|
||||
bar_time: str = Query(
|
||||
"start", description="时间戳: start=bar开始时间(默认) / end=bar结束时间(对齐Tushare)"
|
||||
),
|
||||
adjust: str = Query(
|
||||
"QFQ", description="复权: NONE=不复权 / QFQ=前复权(默认) / HFQ=后复权(需 MAC 客户端)"
|
||||
),
|
||||
mac_client: Any = Depends(get_mac_client_optional),
|
||||
client: Any = Depends(get_client),
|
||||
) -> DataFrameResponse:
|
||||
"""获取股票K线数据。"""
|
||||
"""获取股票K线数据(MAC 协议,支持复权)。
|
||||
|
||||
优先走 AsyncMacClient.get_stock_kline(支持 NONE/QFQ/HFQ 复权 + QFQ 负价兜底);
|
||||
MAC 主机未连接时自动回退 AsyncTdxClient.get_security_bars(无复权,adjust 参数忽略)。
|
||||
输出契约与旧版一致:日线返回 ``date`` 列,分钟线返回 ``datetime`` 列。
|
||||
"""
|
||||
cat = category_from_str(category)
|
||||
if mac_client is not None:
|
||||
period, times = period_times_from_category(cat)
|
||||
df = await mac_client.get_stock_kline(
|
||||
market_value_from_str(market),
|
||||
code,
|
||||
period,
|
||||
start,
|
||||
count,
|
||||
times,
|
||||
adjust=adjust_from_str(adjust),
|
||||
bar_time=bar_time,
|
||||
)
|
||||
# daily_plus:日线及以上周期(DAY=4 及以上)datetime→date
|
||||
df = _normalize_mac_df(df, daily_plus=int(cat) >= int(KlineCategory.DAY))
|
||||
else:
|
||||
# MAC 不可用:回退标准 TdxClient(无复权),adjust 参数忽略
|
||||
_logger.warning(
|
||||
"/bars MAC 客户端未连接,回退标准 TdxClient(不支持复权,adjust=%s 被忽略)",
|
||||
adjust,
|
||||
)
|
||||
df = await client.get_security_bars(
|
||||
market_from_str(market), code, category_from_str(category), start, count, bar_time=bar_time
|
||||
market_from_str(market), code, cat, start, count, bar_time=bar_time
|
||||
)
|
||||
return _df_resp(df)
|
||||
|
||||
|
||||
@@ -35,6 +35,14 @@ class KlineCategoryEnum(IntEnum):
|
||||
SEASON = 10
|
||||
|
||||
|
||||
class AdjustEnum(IntEnum):
|
||||
"""Adjustment type (前复权/后复权)."""
|
||||
|
||||
NONE = 0
|
||||
QFQ = 1 # 前复权
|
||||
HFQ = 2 # 后复权
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Request models
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
@@ -255,6 +255,136 @@ def test_convert_category_invalid_raises_valueerror():
|
||||
category_from_str("INVALID_PERIOD")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# /bars 迁移到 MacClient:KlineCategory→(Period,times) 映射 + adjust 转换
|
||||
# (Issue #43)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_period_times_from_category_mapping():
|
||||
"""KlineCategory → (Period, times) 完整映射,重点 YEAR/SEASON 值不同。"""
|
||||
pytest.importorskip("fastapi")
|
||||
from easy_tdx.mac.enums import Period
|
||||
from easy_tdx.models.enums import KlineCategory
|
||||
from easy_tdx.web.convert import period_times_from_category
|
||||
|
||||
expected = {
|
||||
KlineCategory.MIN_5: (Period.MIN_5, 1),
|
||||
KlineCategory.MIN_15: (Period.MIN_15, 1),
|
||||
KlineCategory.MIN_30: (Period.MIN_30, 1),
|
||||
KlineCategory.MIN_60: (Period.MIN_60, 1),
|
||||
KlineCategory.DAY: (Period.DAILY, 1),
|
||||
KlineCategory.WEEK: (Period.WEEKLY, 1),
|
||||
KlineCategory.MONTH: (Period.MONTHLY, 1),
|
||||
KlineCategory.MIN_1: (Period.MIN_1, 1),
|
||||
KlineCategory.YEAR: (Period.YEARLY, 1), # 值 9 → Period.YEARLY 值 11
|
||||
KlineCategory.SEASON: (Period.QUARTERLY, 1), # SEASON → QUARTERLY
|
||||
}
|
||||
for cat, want in expected.items():
|
||||
assert period_times_from_category(cat) == want, f"{cat} 应映射到 {want}"
|
||||
|
||||
|
||||
def test_period_times_from_category_rejects_unmappable():
|
||||
"""无法映射的 KlineCategory 值(如 MIN_3=8)应抛 ValueError。"""
|
||||
pytest.importorskip("fastapi")
|
||||
from easy_tdx.web.convert import period_times_from_category
|
||||
|
||||
with pytest.raises(ValueError, match="无法映射"):
|
||||
period_times_from_category(8) # MIN_3 不在 /bars 支持范围
|
||||
|
||||
|
||||
def test_adjust_from_str_accepts_name_case_and_int():
|
||||
"""adjust_from_str 支持 NONE/QFQ/HFQ 名称(大小写)和数字字符串。"""
|
||||
pytest.importorskip("fastapi")
|
||||
from easy_tdx.mac.enums import Adjust
|
||||
from easy_tdx.web.convert import adjust_from_str
|
||||
|
||||
assert adjust_from_str("QFQ") == Adjust.QFQ
|
||||
assert adjust_from_str("qfq") == Adjust.QFQ
|
||||
assert adjust_from_str("1") == Adjust.QFQ # 数字字符串
|
||||
assert adjust_from_str("NONE") == Adjust.NONE
|
||||
assert adjust_from_str("none") == Adjust.NONE
|
||||
assert adjust_from_str("0") == Adjust.NONE
|
||||
assert adjust_from_str("HFQ") == Adjust.HFQ
|
||||
assert adjust_from_str("2") == Adjust.HFQ
|
||||
|
||||
|
||||
def test_adjust_from_str_invalid_raises():
|
||||
"""非法复权类型应抛 ValueError。"""
|
||||
pytest.importorskip("fastapi")
|
||||
from easy_tdx.web.convert import adjust_from_str
|
||||
|
||||
with pytest.raises(ValueError, match="无效复权类型"):
|
||||
adjust_from_str("XXX")
|
||||
|
||||
|
||||
def test_normalize_mac_df_daily_plus():
|
||||
"""日线规整:datetime→date(截断时分)、drop float_shares、OHLC 顺序 open/close/high/low。"""
|
||||
pytest.importorskip("fastapi")
|
||||
import pandas as pd
|
||||
|
||||
from easy_tdx.web.routers.bars import _normalize_mac_df
|
||||
|
||||
df = pd.DataFrame(
|
||||
{
|
||||
"datetime": [pd.Timestamp("2026-07-10 15:00:00"), pd.Timestamp("2026-07-11 15:00:00")],
|
||||
"open": [10.0, 10.5],
|
||||
"high": [10.8, 10.9],
|
||||
"low": [9.9, 10.3],
|
||||
"close": [10.5, 10.6],
|
||||
"vol": [1000.0, 1100.0],
|
||||
"amount": [10500.0, 11600.0],
|
||||
"float_shares": [0.0, 0.0],
|
||||
}
|
||||
)
|
||||
out = _normalize_mac_df(df, daily_plus=True)
|
||||
# 时间列:datetime → date,且截断为 00:00:00
|
||||
assert "date" in out.columns
|
||||
assert "datetime" not in out.columns
|
||||
assert out["date"].iloc[0] == pd.Timestamp("2026-07-11 00:00:00") - pd.Timedelta(days=1)
|
||||
# drop float_shares
|
||||
assert "float_shares" not in out.columns
|
||||
# 列顺序:date 在前,OHLC 顺序 open/close/high/low
|
||||
assert list(out.columns) == ["date", "open", "close", "high", "low", "vol", "amount"]
|
||||
|
||||
|
||||
def test_normalize_mac_df_intraday_keeps_datetime():
|
||||
"""分钟线规整:保留 datetime 列(含时分)。"""
|
||||
pytest.importorskip("fastapi")
|
||||
import pandas as pd
|
||||
|
||||
from easy_tdx.web.routers.bars import _normalize_mac_df
|
||||
|
||||
df = pd.DataFrame(
|
||||
{
|
||||
"datetime": [pd.Timestamp("2026-07-10 09:35:00")],
|
||||
"open": [10.0],
|
||||
"high": [10.8],
|
||||
"low": [9.9],
|
||||
"close": [10.5],
|
||||
"vol": [1000.0],
|
||||
"amount": [10500.0],
|
||||
}
|
||||
)
|
||||
out = _normalize_mac_df(df, daily_plus=False)
|
||||
assert "datetime" in out.columns
|
||||
assert "date" not in out.columns
|
||||
# 时分保留
|
||||
assert out["datetime"].iloc[0] == pd.Timestamp("2026-07-10 09:35:00")
|
||||
assert list(out.columns) == ["datetime", "open", "close", "high", "low", "vol", "amount"]
|
||||
|
||||
|
||||
def test_normalize_mac_df_empty_noop():
|
||||
"""空 DataFrame 规整不报错。"""
|
||||
pytest.importorskip("fastapi")
|
||||
import pandas as pd
|
||||
|
||||
from easy_tdx.web.routers.bars import _normalize_mac_df
|
||||
|
||||
out = _normalize_mac_df(pd.DataFrame(), daily_plus=True)
|
||||
assert out.empty
|
||||
|
||||
|
||||
def test_full_app_routes_registered():
|
||||
"""All routers should be mounted and accessible."""
|
||||
pytest.importorskip("fastapi")
|
||||
|
||||
Reference in New Issue
Block a user