diff --git a/api.py b/api.py index 2038bc5..bb9b7ea 100644 --- a/api.py +++ b/api.py @@ -599,43 +599,213 @@ def _split_codes(stocks: str) -> list[str]: return codes -def _simple_endpoint(path: str, fetch, label: str, default_years: int, has_years: bool = True): - """生成统一的 GET 端点: 只有 stocks (+years) 参数的接口.""" - def endpoint( - stocks: str = Query(..., description="股票代码,逗号分隔,例如 '603233.SH'。仅支持代码,不需要 ORG_CODE。", examples=["603233.SH"]), - years: Optional[int] = Query(default_years if has_years else None, ge=1, le=20, description=f"拉取最近多少年的{label}"), - ): - codes = _split_codes(stocks) - df = fetch(codes) if years is None else fetch(codes, years=years) - if df.empty: - extra = {"years": years} if years is not None else {} - return _empty_response(f"无{label}数据, 请检查股票代码或筛选条件", codes, **extra) - payload = _df_to_json_payload(df) - payload["codes_requested"] = codes - if years is not None: - payload["years"] = years - return payload - - app.get(path)(endpoint) - return endpoint - - -# ---- 管理层讨论与分析 / 股本结构 (无 years) ---- -_simple_endpoint("/fin/mda", get_mda, "管理层讨论与分析", default_years=5) -_simple_endpoint("/fin/equity", get_equity, "股本结构", default_years=3, has_years=False) -_simple_endpoint("/fin/company-info", get_company_info, "公司介绍", default_years=3, has_years=False) -_simple_endpoint("/fin/industry", get_industry, "所属行业", default_years=3, has_years=False) -_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) -_simple_endpoint("/fin/index-membership", get_index_membership, "指数成分权重", default_years=3, has_years=False) - -# ---- 重大事项六类 ---- -_simple_endpoint("/fin/acquisitions", get_acquisitions, "并购事件", default_years=3) -_simple_endpoint("/fin/equity-incentive", get_equity_incentive, "股权激励", default_years=3) -_simple_endpoint("/fin/litigation", get_litigation, "诉讼仲裁", default_years=3) -_simple_endpoint("/fin/related-transaction", get_related_transaction, "关联交易", default_years=3) -_simple_endpoint("/fin/violation", get_violation, "违规处罚", default_years=3) -_simple_endpoint("/fin/guarantee", get_guarantee, "对外担保", default_years=3) +@app.get("/fin/mda") +def fin_mda( + stocks: str = Query(..., description="股票代码,逗号分隔,例如 '603233.SH'。仅支持代码,不需要 ORG_CODE。", examples=["603233.SH"]), + years: int = Query(5, ge=1, le=20, description="拉取最近多少年的管理层讨论与分析"), +): + """拉取管理层讨论与分析(年报/中报)。""" + codes = _split_codes(stocks) + df = get_mda(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") +def fin_equity( + stocks: str = Query(..., description="股票代码,逗号分隔,例如 '603233.SH'。仅支持代码,不需要 ORG_CODE。", examples=["603233.SH"]), +): + """拉取股本结构 (全量历史)。""" + codes = _split_codes(stocks) + df = get_equity(codes) + if df.empty: + return _empty_response("无股本结构数据, 请检查股票代码或筛选条件", codes) + + payload = _df_to_json_payload(df) + payload["codes_requested"] = codes + return payload + + +@app.get("/fin/company-info") +def fin_company_info( + stocks: str = Query(..., description="股票代码,逗号分隔,例如 '603233.SH'。仅支持代码,不需要 ORG_CODE。", examples=["603233.SH"]), +): + """拉取公司介绍 (基本资料)。""" + 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__": diff --git a/docs/blueprint.md b/docs/blueprint.md index bd00282..b97762a 100644 --- a/docs/blueprint.md +++ b/docs/blueprint.md @@ -144,7 +144,7 @@ EMWebApi/ 3. **共享**:`from utils.fin_summary import HEADERS, TIMEOUT`,不重复定义;不同 base_url 的接口在本文件内定义新常量;emws 跳转接口用 `_emchoice.get_data_url`,datacenter 报表用 `_emchoice.fetch_report` 4. **数据流**:内部统一 `pandas.DataFrame`,每行冗余 `SECUCODE` 列便于多股票合并 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`) 8. **注释语言**:中文,解释"为什么"(如接口编码映射差异) 9. **测试**:新模块在 `tests/test_interfaces.py` 注册冒烟用例(连真实接口取 1 年数据,断言非空 + 关键列)