api.py 改用显式 @app.get 装饰器注册全部接口

- 移除 _simple_endpoint 程序化注册, 13 个接口改为显式装饰器
- 保留 _split_codes 公共参数解析
- blueprint 施工规范同步: 接口暴露一律使用显式 @app.get
main
Jack 1 month ago
parent 2ce68b7b5d
commit 51ffc6e6cb
  1. 244
      api.py
  2. 2
      docs/blueprint.md

244
api.py

@ -599,43 +599,213 @@ def _split_codes(stocks: str) -> list[str]:
return codes return codes
def _simple_endpoint(path: str, fetch, label: str, default_years: int, has_years: bool = True): @app.get("/fin/mda")
"""生成统一的 GET 端点: 只有 stocks (+years) 参数的接口.""" def fin_mda(
def endpoint( stocks: str = Query(..., description="股票代码,逗号分隔,例如 '603233.SH'。仅支持代码,不需要 ORG_CODE。", examples=["603233.SH"]),
stocks: str = Query(..., description="股票代码,逗号分隔,例如 '603233.SH'。仅支持代码,不需要 ORG_CODE。", examples=["603233.SH"]), years: int = Query(5, ge=1, le=20, description="拉取最近多少年的管理层讨论与分析"),
years: Optional[int] = Query(default_years if has_years else None, ge=1, le=20, description=f"拉取最近多少年的{label}"), ):
): """拉取管理层讨论与分析(年报/中报)。"""
codes = _split_codes(stocks) codes = _split_codes(stocks)
df = fetch(codes) if years is None else fetch(codes, years=years) df = get_mda(codes, years=years)
if df.empty: if df.empty:
extra = {"years": years} if years is not None else {} return _empty_response("无管理层讨论与分析数据, 请检查股票代码或筛选条件", codes, years=years)
return _empty_response(f"{label}数据, 请检查股票代码或筛选条件", codes, **extra)
payload = _df_to_json_payload(df) payload = _df_to_json_payload(df)
payload["codes_requested"] = codes payload["codes_requested"] = codes
if years is not None: payload["years"] = years
payload["years"] = years return payload
return payload
app.get(path)(endpoint) @app.get("/fin/equity")
return endpoint def fin_equity(
stocks: str = Query(..., description="股票代码,逗号分隔,例如 '603233.SH'。仅支持代码,不需要 ORG_CODE。", examples=["603233.SH"]),
):
# ---- 管理层讨论与分析 / 股本结构 (无 years) ---- """拉取股本结构 (全量历史)。"""
_simple_endpoint("/fin/mda", get_mda, "管理层讨论与分析", default_years=5) codes = _split_codes(stocks)
_simple_endpoint("/fin/equity", get_equity, "股本结构", default_years=3, has_years=False) df = get_equity(codes)
_simple_endpoint("/fin/company-info", get_company_info, "公司介绍", default_years=3, has_years=False) if df.empty:
_simple_endpoint("/fin/industry", get_industry, "所属行业", default_years=3, has_years=False) return _empty_response("无股本结构数据, 请检查股票代码或筛选条件", codes)
_simple_endpoint("/fin/index-classif", get_index_classif, "所属指数分类", default_years=3, has_years=False)
_simple_endpoint("/fin/belong-index", get_belong_index, "纳入指数统计", default_years=3, has_years=False) payload = _df_to_json_payload(df)
_simple_endpoint("/fin/index-membership", get_index_membership, "指数成分权重", default_years=3, has_years=False) payload["codes_requested"] = codes
return payload
# ---- 重大事项六类 ----
_simple_endpoint("/fin/acquisitions", get_acquisitions, "并购事件", default_years=3)
_simple_endpoint("/fin/equity-incentive", get_equity_incentive, "股权激励", default_years=3) @app.get("/fin/company-info")
_simple_endpoint("/fin/litigation", get_litigation, "诉讼仲裁", default_years=3) def fin_company_info(
_simple_endpoint("/fin/related-transaction", get_related_transaction, "关联交易", default_years=3) stocks: str = Query(..., description="股票代码,逗号分隔,例如 '603233.SH'。仅支持代码,不需要 ORG_CODE。", examples=["603233.SH"]),
_simple_endpoint("/fin/violation", get_violation, "违规处罚", default_years=3) ):
_simple_endpoint("/fin/guarantee", get_guarantee, "对外担保", default_years=3) """拉取公司介绍 (基本资料)。"""
codes = _split_codes(stocks)
df = get_company_info(codes)
if df.empty:
return _empty_response("无公司介绍数据, 请检查股票代码或筛选条件", codes)
payload = _df_to_json_payload(df)
payload["codes_requested"] = codes
return payload
@app.get("/fin/industry")
def fin_industry(
stocks: str = Query(..., description="股票代码,逗号分隔,例如 '603233.SH'。仅支持代码,不需要 ORG_CODE。", examples=["603233.SH"]),
):
"""拉取所属行业 (当前生效分类)。"""
codes = _split_codes(stocks)
df = get_industry(codes)
if df.empty:
return _empty_response("无所属行业数据, 请检查股票代码或筛选条件", codes)
payload = _df_to_json_payload(df)
payload["codes_requested"] = codes
return payload
@app.get("/fin/index-classif")
def fin_index_classif(
stocks: str = Query(..., description="股票代码,逗号分隔,例如 '603233.SH'。仅支持代码,不需要 ORG_CODE。", examples=["603233.SH"]),
):
"""拉取所属指数分类。"""
codes = _split_codes(stocks)
df = get_index_classif(codes)
if df.empty:
return _empty_response("无所属指数分类数据, 请检查股票代码或筛选条件", codes)
payload = _df_to_json_payload(df)
payload["codes_requested"] = codes
return payload
@app.get("/fin/belong-index")
def fin_belong_index(
stocks: str = Query(..., description="股票代码,逗号分隔,例如 '603233.SH'。仅支持代码,不需要 ORG_CODE。", examples=["603233.SH"]),
):
"""拉取纳入指数统计。"""
codes = _split_codes(stocks)
df = get_belong_index(codes)
if df.empty:
return _empty_response("无纳入指数统计数据, 请检查股票代码或筛选条件", codes)
payload = _df_to_json_payload(df)
payload["codes_requested"] = codes
return payload
@app.get("/fin/index-membership")
def fin_index_membership(
stocks: str = Query(..., description="股票代码,逗号分隔,例如 '603233.SH'。仅支持代码,不需要 ORG_CODE。", examples=["603233.SH"]),
):
"""拉取指数成分权重。"""
codes = _split_codes(stocks)
df = get_index_membership(codes)
if df.empty:
return _empty_response("无指数成分权重数据, 请检查股票代码或筛选条件", codes)
payload = _df_to_json_payload(df)
payload["codes_requested"] = codes
return payload
@app.get("/fin/acquisitions")
def fin_acquisitions(
stocks: str = Query(..., description="股票代码,逗号分隔,例如 '603233.SH'。仅支持代码,不需要 ORG_CODE。", examples=["603233.SH"]),
years: int = Query(3, ge=1, le=20, description="拉取最近多少年的并购事件"),
):
"""拉取并购事件。"""
codes = _split_codes(stocks)
df = get_acquisitions(codes, years=years)
if df.empty:
return _empty_response("无并购事件数据, 请检查股票代码或筛选条件", codes, years=years)
payload = _df_to_json_payload(df)
payload["codes_requested"] = codes
payload["years"] = years
return payload
@app.get("/fin/equity-incentive")
def fin_equity_incentive(
stocks: str = Query(..., description="股票代码,逗号分隔,例如 '603233.SH'。仅支持代码,不需要 ORG_CODE。", examples=["603233.SH"]),
years: int = Query(3, ge=1, le=20, description="拉取最近多少年的股权激励"),
):
"""拉取股权激励方案。"""
codes = _split_codes(stocks)
df = get_equity_incentive(codes, years=years)
if df.empty:
return _empty_response("无股权激励数据, 请检查股票代码或筛选条件", codes, years=years)
payload = _df_to_json_payload(df)
payload["codes_requested"] = codes
payload["years"] = years
return payload
@app.get("/fin/litigation")
def fin_litigation(
stocks: str = Query(..., description="股票代码,逗号分隔,例如 '603233.SH'。仅支持代码,不需要 ORG_CODE。", examples=["603233.SH"]),
years: int = Query(3, ge=1, le=20, description="拉取最近多少年的诉讼仲裁"),
):
"""拉取诉讼仲裁信息。"""
codes = _split_codes(stocks)
df = get_litigation(codes, years=years)
if df.empty:
return _empty_response("无诉讼仲裁数据, 请检查股票代码或筛选条件", codes, years=years)
payload = _df_to_json_payload(df)
payload["codes_requested"] = codes
payload["years"] = years
return payload
@app.get("/fin/related-transaction")
def fin_related_transaction(
stocks: str = Query(..., description="股票代码,逗号分隔,例如 '603233.SH'。仅支持代码,不需要 ORG_CODE。", examples=["603233.SH"]),
years: int = Query(3, ge=1, le=20, description="拉取最近多少年的关联交易"),
):
"""拉取关联交易。"""
codes = _split_codes(stocks)
df = get_related_transaction(codes, years=years)
if df.empty:
return _empty_response("无关联交易数据, 请检查股票代码或筛选条件", codes, years=years)
payload = _df_to_json_payload(df)
payload["codes_requested"] = codes
payload["years"] = years
return payload
@app.get("/fin/violation")
def fin_violation(
stocks: str = Query(..., description="股票代码,逗号分隔,例如 '603233.SH'。仅支持代码,不需要 ORG_CODE。", examples=["603233.SH"]),
years: int = Query(3, ge=1, le=20, description="拉取最近多少年的违规处罚"),
):
"""拉取违规处罚记录。"""
codes = _split_codes(stocks)
df = get_violation(codes, years=years)
if df.empty:
return _empty_response("无违规处罚数据, 请检查股票代码或筛选条件", codes, years=years)
payload = _df_to_json_payload(df)
payload["codes_requested"] = codes
payload["years"] = years
return payload
@app.get("/fin/guarantee")
def fin_guarantee(
stocks: str = Query(..., description="股票代码,逗号分隔,例如 '603233.SH'。仅支持代码,不需要 ORG_CODE。", examples=["603233.SH"]),
years: int = Query(3, ge=1, le=20, description="拉取最近多少年的对外担保"),
):
"""拉取对外担保记录。"""
codes = _split_codes(stocks)
df = get_guarantee(codes, years=years)
if df.empty:
return _empty_response("无对外担保数据, 请检查股票代码或筛选条件", codes, years=years)
payload = _df_to_json_payload(df)
payload["codes_requested"] = codes
payload["years"] = years
return payload
if __name__ == "__main__": if __name__ == "__main__":

@ -144,7 +144,7 @@ EMWebApi/
3. **共享**:`from utils.fin_summary import HEADERS, TIMEOUT`,不重复定义;不同 base_url 的接口在本文件内定义新常量;emws 跳转接口用 `_emchoice.get_data_url`,datacenter 报表用 `_emchoice.fetch_report` 3. **共享**:`from utils.fin_summary import HEADERS, TIMEOUT`,不重复定义;不同 base_url 的接口在本文件内定义新常量;emws 跳转接口用 `_emchoice.get_data_url`,datacenter 报表用 `_emchoice.fetch_report`
4. **数据流**:内部统一 `pandas.DataFrame`,每行冗余 `SECUCODE` 列便于多股票合并 4. **数据流**:内部统一 `pandas.DataFrame`,每行冗余 `SECUCODE` 列便于多股票合并
5. **校验**:`fetch_report` 中 `success=False` 且 message 为"返回数据为空/暂无数据/无数据"时按空结果处理(返回空列表),其余失败抛 `RuntimeError`;空数据返回空 DataFrame 而非报错 5. **校验**:`fetch_report` 中 `success=False` 且 message 为"返回数据为空/暂无数据/无数据"时按空结果处理(返回空列表),其余失败抛 `RuntimeError`;空数据返回空 DataFrame 而非报错
6. **接口暴露**:api.py 中新增 `GET /fin/<主题>`,参数用 FastAPI `Query`,返回 `{success, count, columns, data, errors}` 风格,NaN/NaT/inf 转 None(复用 `_df_to_json_payload`),无数据统一走 `_empty_response``stocks`(+`years`) 参数的接口可用 `_simple_endpoint` 统一生成 6. **接口暴露**:api.py 中每个接口必须用显式 `@app.get` 装饰器 + `Query` 参数(不使用程序化注册),返回 `{success, count, columns, data, errors}` 风格,NaN/NaT/inf 转 None(复用 `_df_to_json_payload`),无数据统一走 `_empty_response``stocks` 参数用 `_split_codes` 解析
7. **礼貌抓取**:批量循环中加随机延时(参考 `SLEEP_RANGE` 7. **礼貌抓取**:批量循环中加随机延时(参考 `SLEEP_RANGE`
8. **注释语言**:中文,解释"为什么"(如接口编码映射差异) 8. **注释语言**:中文,解释"为什么"(如接口编码映射差异)
9. **测试**:新模块在 `tests/test_interfaces.py` 注册冒烟用例(连真实接口取 1 年数据,断言非空 + 关键列) 9. **测试**:新模块在 `tests/test_interfaces.py` 注册冒烟用例(连真实接口取 1 年数据,断言非空 + 关键列)

Loading…
Cancel
Save