mirror of
https://github.com/ZhuLinsen/daily_stock_analysis
synced 2026-09-20 10:53:33 +08:00
feat: 支持 TUSHARE_HTTP_URL 自定义 Tushare Pro 接入地址 (#2048)
* feat: support custom Tushare Pro endpoint via TUSHARE_HTTP_URL Add TUSHARE_HTTP_URL so the Tushare data source can point at a self-hosted or third-party compatible endpoint when the official api.tushare.pro is not reachable. Defaults to the official host when unset, so behavior is unchanged for existing users. - data_provider/tushare_fetcher.py: add _resolve_tushare_http_url() helper (strip + http(s):// schema validation) and forward the resolved URL into _TushareHttpClient, with an info log when a custom endpoint is in use - .env.example + .github/workflows/00-daily-analysis.yml: document and map TUSHARE_HTTP_URL so the new option is wired into the daily job without leaving a half-configured state - tests: cover env parsing (empty/whitespace/http/https/missing schema), fetcher fall-through to the official host, and end-to-end POST target - docs/CHANGELOG.md: flat [Unreleased] entries Fixes #1985 * refactor: drop unnecessary string-literal type hint in _build_api_client TushareHttpClient is already defined above TushareFetcher in the module scope, so a string-literal type hint is not needed for forward reference. Restore the bare type to match the surrounding code style and reduce the diff against main. * docs(tushare): 补 TUSHARE_HTTP_URL 在 full-guide 中英版本的用途/默认行为/workflow 映射说明 按 PR #2048 review 反馈补齐: - 表格内新增 TUSHARE_HTTP_URL 行(中英文版本同步),明确默认 https://api.tushare.pro 与 http(s):// 前缀要求 - 在 GitHub Actions 段落后补充 TUSHARE_HTTP_URL 的 vars/Secrets 优先级与每日 workflow 0映射说明,与现有非敏感配置(TICKFLOW_PRIORITY)一致 - 完整环境变量列表(中英文版本)补 TUSHARE_HTTP_URL 行,默认值列填 https://api.tushare.pro - 与代码实现一致:00-daily-analysis.yml 已用 vars.TUSHARE_HTTP_URL || secrets.TUSHARE_HTTP_URL 映射 * docs(tushare): align TUSHARE_HTTP_URL default with runtime and clarify vars/secrets precedence Per review feedback on PR #2048: the per-repo config contract must stay consistent across runtime, .env.example, workflow priority, tests and both zh/en guides. Two fixes applied as a single contract update: 1. Default endpoint alignment. data_provider/tushare_fetcher.py:100, .env.example, and tests/test_tushare_fetcher_http_client.py all keep the existing official endpoint http://api.tushare.pro, but the zh/en full-guide rows had drifted to https://api.tushare.pro. Switching the documented default to HTTPS would silently change the runtime contract that the feat commit explicitly preserved. Revert both zh and en guide rows to http://api.tushare.pro so docs match runtime, .env.example and the test assertions. 2. vars/secrets precedence wording. The workflow uses 'vars.TUSHARE_HTTP_URL || secrets.TUSHARE_HTTP_URL', which means a non-empty vars entry always wins and Secrets cannot override it. The zh/en notes previously suggested 'Secrets as a tamper fallback' which is incorrect under this precedence and can mislead users into thinking Secrets has override power. Replace with explicit description of the real semantics: vars wins when non-empty; Secrets is only selected when the Variable is empty; for a tamper-resistant deployment put the value only in Secrets and leave Variables empty. Both zh and en guides are updated together; the same 6 contract surfaces (runtime / .env.example / workflow priority / tests / zh guide / en guide) now describe one consistent contract. * docs(tushare): remove false Secret-as-tamper-guard claim, document real vars/secrets write-permission model
This commit is contained in:
@@ -30,6 +30,11 @@ ANSPIRE_API_KEYS=
|
||||
# 数据源配置
|
||||
# Tushare Pro Token(可选,从 https://tushare.pro/weborder/#/login?reg=834638 获取)
|
||||
TUSHARE_TOKEN=
|
||||
# Tushare Pro 自定义接入地址(可选,默认 http://api.tushare.pro)
|
||||
# 适用场景:网络无法直达官方接口时指向自建网关或第三方兼容镜像
|
||||
# 注意:使用非官方接入地址时,Token 与全部请求内容会经过该第三方服务器,请自行评估数据安全风险
|
||||
# 必须以 http:// 或 https:// 开头;留空则保持官方默认地址不变
|
||||
# TUSHARE_HTTP_URL=http://api.tushare.pro
|
||||
# TickFlow API Key(可选;用于 A 股日 K、实时行情、股票列表/名称与大盘复盘增强,权限不足自动回退)
|
||||
# TICKFLOW_API_KEY=
|
||||
# TICKFLOW_KLINE_ADJUST=none # none/forward/backward/forward_additive/backward_additive
|
||||
|
||||
2
.github/workflows/00-daily-analysis.yml
vendored
2
.github/workflows/00-daily-analysis.yml
vendored
@@ -255,6 +255,8 @@ jobs:
|
||||
# 数据源
|
||||
# ==========================================
|
||||
TUSHARE_TOKEN: ${{ secrets.TUSHARE_TOKEN }}
|
||||
# Tushare Pro 自定义接入地址(可选;指向自建网关或第三方兼容镜像时设置)
|
||||
TUSHARE_HTTP_URL: ${{ vars.TUSHARE_HTTP_URL || secrets.TUSHARE_HTTP_URL }}
|
||||
# TickFlow(A 股日 K、实时行情、股票列表/名称与大盘复盘增强;不设则走免费源 fallback)
|
||||
TICKFLOW_API_KEY: ${{ secrets.TICKFLOW_API_KEY }}
|
||||
TICKFLOW_PRIORITY: ${{ vars.TICKFLOW_PRIORITY || secrets.TICKFLOW_PRIORITY }}
|
||||
|
||||
@@ -72,6 +72,28 @@ def _is_us_code(stock_code: str) -> bool:
|
||||
return bool(re.match(r'^[A-Z]{1,5}(\.[A-Z])?$', code))
|
||||
|
||||
|
||||
def _resolve_tushare_http_url() -> Optional[str]:
|
||||
"""读取 ``TUSHARE_HTTP_URL`` 环境变量并做基本校验。
|
||||
|
||||
- 留空 / 仅空白 / 未设置 → 返回 ``None``,调用方继续走官方默认地址。
|
||||
- 设置则去掉首尾空白后返回,并校验必须是 ``http://`` 或 ``https://`` 前缀,
|
||||
避免有人误填成纯主机名(如 ``api.tushare.pro``)导致 ``requests`` 把它
|
||||
当成相对路径请求失败。
|
||||
"""
|
||||
raw = os.getenv("TUSHARE_HTTP_URL")
|
||||
if not raw:
|
||||
return None
|
||||
url = raw.strip()
|
||||
if not url:
|
||||
return None
|
||||
if not (url.startswith("http://") or url.startswith("https://")):
|
||||
raise ValueError(
|
||||
"TUSHARE_HTTP_URL 必须以 http:// 或 https:// 开头,"
|
||||
f"当前值为 {url!r}"
|
||||
)
|
||||
return url
|
||||
|
||||
|
||||
class _TushareHttpClient:
|
||||
"""Lightweight Tushare Pro client that does not require the tushare SDK."""
|
||||
|
||||
@@ -177,7 +199,16 @@ class TushareFetcher(BaseFetcher):
|
||||
|
||||
The project already normalizes all Pro calls through the same request
|
||||
contract, so we do not need the official tushare SDK during runtime.
|
||||
|
||||
支持通过 ``TUSHARE_HTTP_URL`` 环境变量将请求指向自建或第三方兼容
|
||||
端点,便于在网络无法直达 ``api.tushare.pro`` 时切换镜像/网关。
|
||||
留空或不设置则保持官方默认地址,行为与历史版本完全一致。
|
||||
"""
|
||||
api_url = _resolve_tushare_http_url()
|
||||
if api_url:
|
||||
logger.info("Tushare 使用自定义接入地址: %s", api_url)
|
||||
client = _TushareHttpClient(token=token, api_url=api_url)
|
||||
else:
|
||||
client = _TushareHttpClient(token=token)
|
||||
logger.debug("Tushare API client configured for direct HTTP calls")
|
||||
return client
|
||||
|
||||
@@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
||||
<!-- 新条目格式:- [类型] 描述(类型取值:新功能/改进/修复/文档/测试/chore)-->
|
||||
<!-- 每条独立一行追加到本段末尾,无需分类标题,合并时冲突最小 -->
|
||||
- [修复] #2026 外股代码映射到中文显示名时英文新闻相关性判定漏判:新增同源 STOCK_ENGLISH_NAME_MAP 单一真源、canonicalize_foreign_stock_code 规范化入口与 _foreign_english_query_terms 别名解析,使 AAPL/00700/BABA 等 ticker 即使 stock_name 为中文也能在查询构建、相关性打分与多维度情报路径上复用 canonical 英文名,并补齐 .US/.HK suffix / HK 前缀全形式的归类与回归用例;同时在 _score_news_relevance 对 alias 展开 term 做去重,避免 legal alias 展开短名与显式 short alias 重复计分。
|
||||
- [新功能] Tushare 数据源支持通过 `TUSHARE_HTTP_URL` 环境变量自定义接入地址,便于网络无法直达 `api.tushare.pro` 时切换自建网关或第三方兼容镜像;留空保持官方默认地址不变(fixes #1985)
|
||||
- [文档] `.env.example` 与 `.github/workflows/00-daily-analysis.yml` 同步映射 `TUSHARE_HTTP_URL`,避免出现"配置项有但 workflow 漏映射"的半修状态
|
||||
- [修复] #2051 PR Review 的特权 `pull_request_target` 流程不再检出 fork PR head:敏感文件、标签、报告与 AI 审查统一通过 GitHub API 将 PR 元数据和 diff 作为数据读取,只执行主分支可信脚本;Python 语法、Flake8、确定性检查和离线测试继续由无 secrets 的 `pull_request` CI / `backend-gate` 执行,兼容 `actions/checkout` 新增的 fork checkout 安全保护。
|
||||
|
||||
## [3.27.0] - 2026-07-19
|
||||
|
||||
@@ -163,6 +163,7 @@ daily_stock_analysis/
|
||||
| `SEARXNG_BASE_URLS` | SearXNG 自建实例(无配额兜底,需在 settings.yml 启用 format: json);留空时默认自动发现公共实例 | 可选 |
|
||||
| `SEARXNG_PUBLIC_INSTANCES_ENABLED` | 是否在 `SEARXNG_BASE_URLS` 为空时自动从 `searx.space` 获取公共实例(默认 `true`) | 可选 |
|
||||
| `TUSHARE_TOKEN` | [Tushare Pro](https://tushare.pro/weborder/#/login?reg=834638 ) Token | 可选 |
|
||||
| `TUSHARE_HTTP_URL` | Tushare Pro HTTP 接入地址;留空(或未设置/空白)时使用官方端点 `http://api.tushare.pro`,仅在需通过公司内网代理、跨境网络或自建镜像时填写 `http://` 或 `https://` 开头的完整地址 | 可选 |
|
||||
| `TICKFLOW_API_KEY` | [TickFlow](https://tickflow.org) API Key;可选,用于 A 股日 K、实时行情、股票列表/名称与大盘复盘增强;失败或权限不足时自动回退。 | 可选 |
|
||||
| `LONGBRIDGE_OAUTH_CLIENT_ID` | [Longbridge OpenAPI](https://open.longbridge.com/) OAuth client_id;留空且无 Legacy Access Token 时会兼容使用 `LONGBRIDGE_APP_KEY` | 可选 |
|
||||
| `LONGBRIDGE_OAUTH_TOKEN_CACHE_B64` | OAuth token 缓存文件的 base64 内容,供 GitHub Actions / Docker 等 headless 环境恢复 SDK token 缓存 | 可选 |
|
||||
@@ -182,6 +183,8 @@ daily_stock_analysis/
|
||||
|
||||
> **GitHub Actions:** 仓库自带 `00-daily-analysis.yml` 已把 `TUSHARE_TOKEN`、`TICKFLOW_API_KEY` / `TICKFLOW_*` 和上表中的 `LONGBRIDGE_*` 映射到任务环境。TickFlow 的 API Key 建议放在 **Secrets**,优先级、复权和批量开关可放在 **Variables** 或 **Secrets**。Longbridge OAuth 方式需要一个 client_id(优先 `LONGBRIDGE_OAUTH_CLIENT_ID`;留空且无 Legacy Access Token 时使用 `LONGBRIDGE_APP_KEY` 兼容),并把本机 `~/.longbridge/openapi/tokens/<client_id>` 文件 base64 后保存为 Secret `LONGBRIDGE_OAUTH_TOKEN_CACHE_B64`;Legacy 方式仍可配置 `LONGBRIDGE_APP_KEY`、`LONGBRIDGE_APP_SECRET`、`LONGBRIDGE_ACCESS_TOKEN`。可选接入点变量(如 `LONGBRIDGE_REGION`)可放在 **Variables** 或 **Secrets**。
|
||||
|
||||
> **TUSHARE_HTTP_URL 在每日 workflow 中的映射:** `00-daily-analysis.yml` 已显式映射 `TUSHARE_HTTP_URL`(采用 `vars.TUSHARE_HTTP_URL || secrets.TUSHARE_HTTP_URL` 优先级,与仓库现有 `TICKFLOW_PRIORITY` 等非敏感配置取值策略一致)。该地址属"接入地址"配置而非凭据,建议放 **Variables** 便于团队 review 与版本可审计。注意:真实的优先级是 `vars` 非空即胜出,**Secrets 中的同名变量无法覆盖非空 Variables**,两者中只有 vars 为空时 secrets 才被采用,请按这个真实语义做安全建模。GitHub 把 Variables 与 Secrets 设计为两套独立的写权限模型:任何对 repository Variables 有写权限的人或自动化,都可以在不读取、不修改 Secrets 的情况下,通过设置一个非空 Variable 来改写运行时端点(包括将 `TUSHARE_TOKEN` 和完整请求体指向攻击者控制的地址);Secrets 仅保护值的机密性,并不自动提供"端点完整性"或"优先级覆盖"保障。如确需对端点施加更强的访问控制,请使用 GitHub Environment protection rules、CODEOWNERS、branch protection 或独立的部署审批流程,**不要把"只放 Secrets 而 Variables 留空"当作防改护栏**。未设置或留空时 fetcher 仍走官方 `http://api.tushare.pro` 端点,不会因为本变量缺失而报错。
|
||||
|
||||
> **Longbridge 运行时行为:** 未配置凭据时不会实例化 Longbridge 这个可选 fetcher;若运行时遇到 `client is closed`、`context closed`、`connection closed` 等连接关闭类异常,会进入冷却期(默认 15 秒,可用 `LONGBRIDGE_CONNECTION_COOLDOWN_SECONDS` 调整),冷却期内美股/港股的实时与日线请求会自动跳过 Longbridge,退回 YFinance / AkShare 等兜底链路。
|
||||
|
||||
> 补充说明
|
||||
@@ -405,6 +408,7 @@ daily_stock_analysis/
|
||||
| 变量名 | 说明 | 默认值 | 必填 |
|
||||
|--------|------|--------|:----:|
|
||||
| `TUSHARE_TOKEN` | Tushare Pro Token | - | 可选 |
|
||||
| `TUSHARE_HTTP_URL` | Tushare Pro HTTP 接入地址;留空时使用官方端点 `http://api.tushare.pro`,仅在需通过公司内网代理、跨境网络或自建镜像时填 `http://` 或 `https://` 开头的完整地址 | `http://api.tushare.pro` | 可选 |
|
||||
| `TICKFLOW_API_KEY` | TickFlow API Key;可选,用于 A 股日 K、实时行情、股票列表/名称与大盘复盘增强;失败或权限不足时自动回退。 | - | 可选 |
|
||||
| `TICKFLOW_PRIORITY` | TickFlow 日 K 数据源优先级;数字越小越早尝试,默认 `2`;未配置 API Key 时不启用;不影响实时行情,实时行情顺序由 `REALTIME_SOURCE_PRIORITY` 控制。 | `2` | 可选 |
|
||||
| `TICKFLOW_KLINE_ADJUST` | TickFlow 日 K 复权模式:`none`、`forward`、`backward`、`forward_additive`、`backward_additive`。 | `none` | 可选 |
|
||||
|
||||
@@ -153,10 +153,13 @@ Go to your forked repo → `Settings` → `Secrets and variables` → `Actions`
|
||||
| `SEARXNG_BASE_URLS` | SearXNG self-hosted instances (quota-free fallback, enable format: json in settings.yml); when empty the app auto-discovers public instances | Optional |
|
||||
| `SEARXNG_PUBLIC_INSTANCES_ENABLED` | Auto-discover public SearXNG instances from `searx.space` when `SEARXNG_BASE_URLS` is empty (default `true`) | Optional |
|
||||
| `TUSHARE_TOKEN` | [Tushare Pro](https://tushare.pro/weborder/#/login?reg=834638) Token | Optional |
|
||||
| `TUSHARE_HTTP_URL` | Tushare Pro HTTP endpoint; when unset/empty defaults to the official `http://api.tushare.pro`. Set to a `http://` or `https://` URL only when routing through a corporate proxy, cross-border network, or a self-hosted mirror | Optional |
|
||||
| `TICKFLOW_API_KEY` | [TickFlow](https://tickflow.org) API key for optional A-share daily K-lines, realtime quotes, stock list/name lookup, and CN market review enhancement; permission or entitlement failures fall back to existing providers | Optional |
|
||||
|
||||
> **GitHub Actions:** The bundled `00-daily-analysis.yml` maps `TUSHARE_TOKEN`, `TICKFLOW_API_KEY` / `TICKFLOW_*`, and the documented `LONGBRIDGE_*` variables into the job environment. Store `TICKFLOW_API_KEY` in **Secrets**; non-sensitive TickFlow priority, adjustment, and batch switches can live in **Variables** or **Secrets**. Longbridge OAuth still requires a client id plus `LONGBRIDGE_OAUTH_TOKEN_CACHE_B64` for headless Actions runs, while the legacy `LONGBRIDGE_APP_KEY` / `LONGBRIDGE_APP_SECRET` / `LONGBRIDGE_ACCESS_TOKEN` triplet remains supported.
|
||||
|
||||
> **`TUSHARE_HTTP_URL` mapping in the daily workflow:** `00-daily-analysis.yml` maps `TUSHARE_HTTP_URL` with `vars.TUSHARE_HTTP_URL || secrets.TUSHARE_HTTP_URL` — the same vars-first precedence used for `TICKFLOW_PRIORITY` and other non-sensitive knobs. The endpoint is an "address" rather than a credential, so **Variables** is preferred for team review and audit visibility. Note that the real precedence is "vars wins when non-empty": a **Secrets** entry with the same name is **not** a tamper fallback that overrides a non-empty Variable — Secrets is only selected when the Variable is empty. Build your threat model on that actual semantics. GitHub exposes Variables and Secrets as two independent write-permission models: anyone (human or automation) with write access to repository Variables can set a non-empty Variable and silently reroute the runtime endpoint — including `TUSHARE_TOKEN` and the full request body to an attacker-controlled URL — without reading or modifying any Secret. Secrets only protect value confidentiality; they do **not** provide "endpoint integrity" or "priority override" guarantees. If you need stronger access control over the endpoint, use GitHub Environment protection rules, CODEOWNERS, branch protection, or a dedicated deployment approval flow — **do not treat "put it in Secrets and leave Variables empty" as a tamper guard**. Leaving it unset or empty preserves the default `http://api.tushare.pro` endpoint — the fetcher does not error when this variable is missing.
|
||||
|
||||
#### ✅ Minimum Configuration Example
|
||||
|
||||
To get started quickly, you need at minimum:
|
||||
@@ -343,6 +346,7 @@ For the notification baseline, diagnostics, and deployment notes, see [Notificat
|
||||
| Variable | Description | Default | Required |
|
||||
|--------|------|--------|:----:|
|
||||
| `TUSHARE_TOKEN` | Tushare Pro Token | - | Optional |
|
||||
| `TUSHARE_HTTP_URL` | Tushare Pro HTTP endpoint; defaults to `http://api.tushare.pro` when unset/empty. Set only when routing through a corporate proxy, cross-border network, or a self-hosted mirror (must start with `http://` or `https://`). | `http://api.tushare.pro` | Optional |
|
||||
| `TICKFLOW_API_KEY` | TickFlow API key; enables optional A-share daily K-lines, realtime quotes, stock list/name lookup, and CN market review enhancement. Permission failures fall back to existing providers. | - | Optional |
|
||||
| `TICKFLOW_PRIORITY` | TickFlow daily K-line provider priority; lower values are tried earlier. No effect unless `TICKFLOW_API_KEY` is configured. Does not affect realtime quotes, which are ordered by `REALTIME_SOURCE_PRIORITY`. | `2` | Optional |
|
||||
| `TICKFLOW_KLINE_ADJUST` | TickFlow daily K-line adjustment mode: `none`, `forward`, `backward`, `forward_additive`, or `backward_additive`. | `none` | Optional |
|
||||
|
||||
@@ -20,7 +20,11 @@ except ValueError:
|
||||
if not json_repair_available and "json_repair" not in sys.modules:
|
||||
sys.modules["json_repair"] = MagicMock()
|
||||
|
||||
from data_provider.tushare_fetcher import TushareFetcher, _TushareHttpClient
|
||||
from data_provider.tushare_fetcher import (
|
||||
TushareFetcher,
|
||||
_TushareHttpClient,
|
||||
_resolve_tushare_http_url,
|
||||
)
|
||||
|
||||
|
||||
class TestTushareHttpClient(unittest.TestCase):
|
||||
@@ -75,5 +79,97 @@ class TestTushareFetcherInit(unittest.TestCase):
|
||||
self.assertEqual(fetcher.priority, -1)
|
||||
|
||||
|
||||
class TestResolveTushareHttpUrl(unittest.TestCase):
|
||||
"""``TUSHARE_HTTP_URL`` 环境变量解析与校验。"""
|
||||
|
||||
def test_unset_returns_none(self) -> None:
|
||||
with patch.dict("os.environ", {}, clear=False):
|
||||
import os
|
||||
os.environ.pop("TUSHARE_HTTP_URL", None)
|
||||
self.assertIsNone(_resolve_tushare_http_url())
|
||||
|
||||
def test_empty_or_whitespace_returns_none(self) -> None:
|
||||
with patch.dict("os.environ", {"TUSHARE_HTTP_URL": " "}):
|
||||
self.assertIsNone(_resolve_tushare_http_url())
|
||||
|
||||
def test_valid_http_url_returned_stripped(self) -> None:
|
||||
with patch.dict("os.environ", {"TUSHARE_HTTP_URL": " http://gw.example.com/tushare "}):
|
||||
self.assertEqual(_resolve_tushare_http_url(), "http://gw.example.com/tushare")
|
||||
|
||||
def test_https_url_returned(self) -> None:
|
||||
with patch.dict("os.environ", {"TUSHARE_HTTP_URL": "https://gw.example.com/tushare"}):
|
||||
self.assertEqual(_resolve_tushare_http_url(), "https://gw.example.com/tushare")
|
||||
|
||||
def test_missing_schema_raises_value_error(self) -> None:
|
||||
# 防止有人误填纯主机名(如 'api.tushare.pro')后被 requests 当成相对路径
|
||||
with patch.dict("os.environ", {"TUSHARE_HTTP_URL": "gw.example.com"}):
|
||||
with self.assertRaises(ValueError):
|
||||
_resolve_tushare_http_url()
|
||||
|
||||
|
||||
class TestTushareFetcherCustomHttpUrl(unittest.TestCase):
|
||||
"""``TUSHARE_HTTP_URL`` 真正打通到 HTTP client 的接入地址。"""
|
||||
|
||||
def test_fetcher_uses_custom_url_when_env_set(self) -> None:
|
||||
config = SimpleNamespace(tushare_token="demo-token")
|
||||
|
||||
with patch("data_provider.tushare_fetcher.get_config", return_value=config), \
|
||||
patch.dict("os.environ", {"TUSHARE_HTTP_URL": "http://gw.example.com/tushare"}):
|
||||
fetcher = TushareFetcher()
|
||||
|
||||
self.assertIsInstance(fetcher._api, _TushareHttpClient)
|
||||
self.assertEqual(fetcher._api._api_url, "http://gw.example.com/tushare")
|
||||
|
||||
def test_fetcher_falls_back_to_official_when_env_empty(self) -> None:
|
||||
config = SimpleNamespace(tushare_token="demo-token")
|
||||
|
||||
env = {k: v for k, v in __import__("os").environ.items() if k != "TUSHARE_HTTP_URL"}
|
||||
with patch("data_provider.tushare_fetcher.get_config", return_value=config), \
|
||||
patch.dict("os.environ", env, clear=True):
|
||||
fetcher = TushareFetcher()
|
||||
|
||||
self.assertIsInstance(fetcher._api, _TushareHttpClient)
|
||||
self.assertEqual(fetcher._api._api_url, "http://api.tushare.pro")
|
||||
|
||||
def test_query_posts_to_custom_endpoint(self) -> None:
|
||||
"""端到端确保自定义 url 真正驱动 requests.post 的目标地址。"""
|
||||
config = SimpleNamespace(tushare_token="demo-token")
|
||||
|
||||
with patch("data_provider.tushare_fetcher.get_config", return_value=config), \
|
||||
patch.dict("os.environ", {"TUSHARE_HTTP_URL": "http://gw.example.com/tushare"}):
|
||||
fetcher = TushareFetcher()
|
||||
|
||||
response = MagicMock(
|
||||
status_code=200,
|
||||
text=json.dumps(
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"fields": ["ts_code", "close"],
|
||||
"items": [["600519.SH", 1688.0]],
|
||||
},
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
with patch("data_provider.tushare_fetcher.requests.post", return_value=response) as post_mock:
|
||||
fetcher._api.daily(ts_code="600519.SH", start_date="20260320", end_date="20260325")
|
||||
|
||||
post_mock.assert_called_once_with(
|
||||
"http://gw.example.com/tushare",
|
||||
json={
|
||||
"api_name": "daily",
|
||||
"token": "demo-token",
|
||||
"params": {
|
||||
"ts_code": "600519.SH",
|
||||
"start_date": "20260320",
|
||||
"end_date": "20260325",
|
||||
},
|
||||
"fields": "",
|
||||
},
|
||||
timeout=30,
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user