akshare-cli 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- akshare_cli/__init__.py +3 -0
- akshare_cli/cli.py +1285 -0
- akshare_cli/core/__init__.py +1 -0
- akshare_cli/core/cache.py +127 -0
- akshare_cli/core/export.py +115 -0
- akshare_cli/core/registry.py +236 -0
- akshare_cli/core/session.py +115 -0
- akshare_cli/tests/__init__.py +1 -0
- akshare_cli/tests/conftest.py +37 -0
- akshare_cli/tests/test_cache.py +111 -0
- akshare_cli/tests/test_core.py +439 -0
- akshare_cli/tests/test_doc_examples.py +636 -0
- akshare_cli/tests/test_full_e2e.py +262 -0
- akshare_cli/utils/__init__.py +1 -0
- akshare_cli/utils/formatting.py +62 -0
- akshare_cli-0.2.0.dist-info/METADATA +1212 -0
- akshare_cli-0.2.0.dist-info/RECORD +20 -0
- akshare_cli-0.2.0.dist-info/WHEEL +5 -0
- akshare_cli-0.2.0.dist-info/entry_points.txt +2 -0
- akshare_cli-0.2.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,636 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Documentation-based E2E tests for AKShare CLI harness.
|
|
3
|
+
|
|
4
|
+
Tests are derived from the official akshare documentation:
|
|
5
|
+
https://akshare.akfamily.xyz/data/index.html
|
|
6
|
+
|
|
7
|
+
Each test calls a real akshare function via the CLI and validates:
|
|
8
|
+
1. Exit code is 0 (success)
|
|
9
|
+
2. JSON output is parseable
|
|
10
|
+
3. Returned data has expected structure (total_rows, columns, data)
|
|
11
|
+
|
|
12
|
+
All tests require network access and are marked with @pytest.mark.network.
|
|
13
|
+
Run with: pytest -m network test_doc_examples.py -v
|
|
14
|
+
Skip with: pytest -m "not network" ...
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import shutil
|
|
20
|
+
import subprocess
|
|
21
|
+
import sys
|
|
22
|
+
import tempfile
|
|
23
|
+
|
|
24
|
+
import pytest
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
# ─── Shared helpers ──────────────────────────────────────────────────────────
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _resolve_cli(name: str = "akshare-cli") -> str:
|
|
31
|
+
"""Resolve CLI path: venv -> system PATH -> module fallback."""
|
|
32
|
+
# Try venv first
|
|
33
|
+
venv_bin = os.path.join(
|
|
34
|
+
os.path.dirname(os.path.dirname(os.path.dirname(
|
|
35
|
+
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
36
|
+
))),
|
|
37
|
+
".venv", "bin", name,
|
|
38
|
+
)
|
|
39
|
+
if os.path.isfile(venv_bin):
|
|
40
|
+
return venv_bin
|
|
41
|
+
path = shutil.which(name)
|
|
42
|
+
if path:
|
|
43
|
+
return path
|
|
44
|
+
return None
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def run_cli(args: list, timeout: int = 60) -> subprocess.CompletedProcess:
|
|
48
|
+
"""Run CLI and return result."""
|
|
49
|
+
cli_path = _resolve_cli()
|
|
50
|
+
if cli_path:
|
|
51
|
+
cmd = [cli_path] + args
|
|
52
|
+
else:
|
|
53
|
+
cmd = [sys.executable, "-m", "akshare_cli.cli"] + args
|
|
54
|
+
return subprocess.run(cmd, capture_output=True, text=True, timeout=timeout)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def run_json(args: list, timeout: int = 60) -> dict:
|
|
58
|
+
"""Run CLI with --json and return parsed JSON dict."""
|
|
59
|
+
result = run_cli(["--json"] + args, timeout=timeout)
|
|
60
|
+
assert result.returncode == 0, (
|
|
61
|
+
f"CLI failed (exit {result.returncode}): {result.stdout[:500]} {result.stderr[:500]}"
|
|
62
|
+
)
|
|
63
|
+
# Strip any progress bar lines (tqdm output on stdout)
|
|
64
|
+
stdout = result.stdout.strip()
|
|
65
|
+
# Find the first '{' which marks the start of JSON
|
|
66
|
+
idx = stdout.find("{")
|
|
67
|
+
assert idx != -1, f"No JSON object found in output: {stdout[:300]}"
|
|
68
|
+
return json.loads(stdout[idx:])
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def assert_dataframe_json(data: dict, min_rows: int = 1):
|
|
72
|
+
"""Assert that the JSON output has standard DataFrame structure."""
|
|
73
|
+
assert "total_rows" in data, f"Missing 'total_rows' in: {list(data.keys())}"
|
|
74
|
+
assert "columns" in data, f"Missing 'columns' in: {list(data.keys())}"
|
|
75
|
+
assert "data" in data, f"Missing 'data' in: {list(data.keys())}"
|
|
76
|
+
assert data["total_rows"] >= min_rows, (
|
|
77
|
+
f"Expected >= {min_rows} rows, got {data['total_rows']}"
|
|
78
|
+
)
|
|
79
|
+
assert len(data["columns"]) > 0, "No columns returned"
|
|
80
|
+
assert len(data["data"]) >= min_rows, (
|
|
81
|
+
f"Expected >= {min_rows} data entries, got {len(data['data'])}"
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
# ─── Stock Tests ─────────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@pytest.mark.network
|
|
89
|
+
class TestStockCommands:
|
|
90
|
+
"""Test stock-related CLI commands from documentation."""
|
|
91
|
+
|
|
92
|
+
def test_stock_sse_summary(self):
|
|
93
|
+
"""上海证券交易所-股票数据总貌"""
|
|
94
|
+
data = run_json(["call", "stock_sse_summary"])
|
|
95
|
+
assert_dataframe_json(data)
|
|
96
|
+
assert "项目" in data["columns"] or "item" in str(data["columns"]).lower()
|
|
97
|
+
|
|
98
|
+
def test_stock_szse_summary(self):
|
|
99
|
+
"""深圳证券交易所-市场总貌"""
|
|
100
|
+
data = run_json(["call", "stock_szse_summary", "--date", "20200619"])
|
|
101
|
+
assert_dataframe_json(data)
|
|
102
|
+
|
|
103
|
+
def test_stock_individual_info_em(self):
|
|
104
|
+
"""东方财富-个股信息查询 (000001 平安银行)"""
|
|
105
|
+
data = run_json(["call", "stock_individual_info_em", "--symbol", "000001"])
|
|
106
|
+
assert_dataframe_json(data)
|
|
107
|
+
# Check that it contains stock code info
|
|
108
|
+
items = [row.get("item", "") for row in data["data"]]
|
|
109
|
+
assert any("股票" in str(item) or "代码" in str(item) for item in items), (
|
|
110
|
+
f"Expected stock code info in items: {items}"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
def test_stock_bid_ask_em(self):
|
|
114
|
+
"""东方财富-五档盘口"""
|
|
115
|
+
data = run_json(["call", "stock_bid_ask_em", "--symbol", "000001"])
|
|
116
|
+
assert_dataframe_json(data)
|
|
117
|
+
|
|
118
|
+
@pytest.mark.slow
|
|
119
|
+
def test_stock_zh_a_spot_em(self):
|
|
120
|
+
"""东方财富-A股实时行情 (slow: 5000+ stocks, API often times out)"""
|
|
121
|
+
data = run_json(["call", "stock_zh_a_spot_em", "--limit", "5"], timeout=120)
|
|
122
|
+
assert_dataframe_json(data, min_rows=5)
|
|
123
|
+
|
|
124
|
+
def test_stock_zh_a_hist(self):
|
|
125
|
+
"""东方财富-A股历史行情-日线 (000001 平安银行)"""
|
|
126
|
+
data = run_json([
|
|
127
|
+
"call", "stock_zh_a_hist",
|
|
128
|
+
"--symbol", "000001",
|
|
129
|
+
"--period", "daily",
|
|
130
|
+
"--start_date", "20240101",
|
|
131
|
+
"--end_date", "20240110",
|
|
132
|
+
"--adjust", "",
|
|
133
|
+
])
|
|
134
|
+
assert_dataframe_json(data)
|
|
135
|
+
assert "日期" in data["columns"] or "date" in str(data["columns"]).lower()
|
|
136
|
+
|
|
137
|
+
def test_stock_zh_a_hist_qfq(self):
|
|
138
|
+
"""东方财富-A股历史行情-前复权"""
|
|
139
|
+
data = run_json([
|
|
140
|
+
"call", "stock_zh_a_hist",
|
|
141
|
+
"--symbol", "600519",
|
|
142
|
+
"--period", "daily",
|
|
143
|
+
"--start_date", "20240101",
|
|
144
|
+
"--end_date", "20240110",
|
|
145
|
+
"--adjust", "qfq",
|
|
146
|
+
])
|
|
147
|
+
assert_dataframe_json(data)
|
|
148
|
+
|
|
149
|
+
@pytest.mark.slow
|
|
150
|
+
def test_stock_zh_a_st_em(self):
|
|
151
|
+
"""东方财富-风险警示板-ST股票 (slow: API often times out)"""
|
|
152
|
+
data = run_json(["call", "stock_zh_a_st_em", "--limit", "5"], timeout=120)
|
|
153
|
+
assert_dataframe_json(data)
|
|
154
|
+
|
|
155
|
+
@pytest.mark.slow
|
|
156
|
+
def test_stock_zh_b_spot_em(self):
|
|
157
|
+
"""东方财富-B股实时行情 (slow: API often times out)"""
|
|
158
|
+
data = run_json(["call", "stock_zh_b_spot_em", "--limit", "5"], timeout=120)
|
|
159
|
+
assert_dataframe_json(data)
|
|
160
|
+
|
|
161
|
+
def test_stock_sse_deal_daily(self):
|
|
162
|
+
"""上海证券交易所-每日概况"""
|
|
163
|
+
data = run_json(["call", "stock_sse_deal_daily", "--date", "20250221"])
|
|
164
|
+
assert_dataframe_json(data)
|
|
165
|
+
|
|
166
|
+
@pytest.mark.slow
|
|
167
|
+
def test_stock_zh_a_new_em(self):
|
|
168
|
+
"""东方财富-新股实时行情 (slow: API often times out)"""
|
|
169
|
+
data = run_json(["call", "stock_zh_a_new_em", "--limit", "5"], timeout=120)
|
|
170
|
+
assert_dataframe_json(data)
|
|
171
|
+
|
|
172
|
+
@pytest.mark.slow
|
|
173
|
+
def test_stock_intraday_em(self):
|
|
174
|
+
"""东方财富-盘口异动 (slow: API often disconnects)"""
|
|
175
|
+
data = run_json(["call", "stock_intraday_em", "--symbol", "000001"])
|
|
176
|
+
assert_dataframe_json(data)
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
# ─── Stock Shortcut Tests ────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
@pytest.mark.network
|
|
183
|
+
class TestStockShortcuts:
|
|
184
|
+
"""Test stock shortcut commands (stock hist, stock spot)."""
|
|
185
|
+
|
|
186
|
+
def test_stock_hist(self):
|
|
187
|
+
"""Shortcut: stock hist 000001 --json"""
|
|
188
|
+
data = run_json([
|
|
189
|
+
"stock", "hist", "000001",
|
|
190
|
+
"--start", "20240101", "--end", "20240110",
|
|
191
|
+
])
|
|
192
|
+
assert_dataframe_json(data)
|
|
193
|
+
|
|
194
|
+
def test_stock_hist_weekly(self):
|
|
195
|
+
"""Shortcut: stock hist --period weekly"""
|
|
196
|
+
data = run_json([
|
|
197
|
+
"stock", "hist", "600519",
|
|
198
|
+
"--period", "weekly",
|
|
199
|
+
"--start", "20240101", "--end", "20240301",
|
|
200
|
+
])
|
|
201
|
+
assert_dataframe_json(data)
|
|
202
|
+
|
|
203
|
+
def test_stock_hist_qfq(self):
|
|
204
|
+
"""Shortcut: stock hist --adjust qfq"""
|
|
205
|
+
data = run_json([
|
|
206
|
+
"stock", "hist", "000001",
|
|
207
|
+
"--adjust", "qfq",
|
|
208
|
+
"--start", "20240101", "--end", "20240110",
|
|
209
|
+
])
|
|
210
|
+
assert_dataframe_json(data)
|
|
211
|
+
|
|
212
|
+
@pytest.mark.slow
|
|
213
|
+
def test_stock_spot(self):
|
|
214
|
+
"""Shortcut: stock spot --json (slow: all A-shares, large dataset)"""
|
|
215
|
+
data = run_json(["stock", "spot", "--limit", "5"], timeout=120)
|
|
216
|
+
assert_dataframe_json(data, min_rows=5)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
# ─── Futures Tests ───────────────────────────────────────────────────────────
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
@pytest.mark.network
|
|
223
|
+
class TestFuturesCommands:
|
|
224
|
+
"""Test futures-related CLI commands from documentation."""
|
|
225
|
+
|
|
226
|
+
def test_futures_fees_info(self):
|
|
227
|
+
"""期货交易费用参照表"""
|
|
228
|
+
data = run_json(["call", "futures_fees_info", "--limit", "5"])
|
|
229
|
+
assert_dataframe_json(data)
|
|
230
|
+
assert "合约代码" in data["columns"] or "交易所" in data["columns"]
|
|
231
|
+
|
|
232
|
+
@pytest.mark.slow
|
|
233
|
+
def test_futures_hist_em(self):
|
|
234
|
+
"""东方财富-期货历史行情 (slow: fetches full history then limits)"""
|
|
235
|
+
data = run_json([
|
|
236
|
+
"call", "futures_hist_em",
|
|
237
|
+
"--symbol", "热卷主连",
|
|
238
|
+
"--period", "daily",
|
|
239
|
+
"--limit", "5",
|
|
240
|
+
], timeout=120)
|
|
241
|
+
assert_dataframe_json(data)
|
|
242
|
+
|
|
243
|
+
@pytest.mark.slow
|
|
244
|
+
def test_futures_hist_table_em(self):
|
|
245
|
+
"""东方财富-期货合约表 (slow: large dataset)"""
|
|
246
|
+
data = run_json(["call", "futures_hist_table_em", "--limit", "5"], timeout=120)
|
|
247
|
+
assert_dataframe_json(data)
|
|
248
|
+
|
|
249
|
+
def test_futures_contract_info_gfex(self):
|
|
250
|
+
"""广州期货交易所-合约信息"""
|
|
251
|
+
data = run_json(["call", "futures_contract_info_gfex"])
|
|
252
|
+
assert_dataframe_json(data)
|
|
253
|
+
|
|
254
|
+
def test_futures_symbol_mark(self):
|
|
255
|
+
"""期货品种标记表"""
|
|
256
|
+
data = run_json(["call", "futures_symbol_mark"])
|
|
257
|
+
assert_dataframe_json(data)
|
|
258
|
+
|
|
259
|
+
def test_futures_spot_price(self):
|
|
260
|
+
"""现货价格和基差"""
|
|
261
|
+
data = run_json(["call", "futures_spot_price", "--date", "20240430"])
|
|
262
|
+
assert_dataframe_json(data)
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
# ─── Futures Shortcut Tests ──────────────────────────────────────────────────
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
@pytest.mark.network
|
|
269
|
+
class TestFuturesShortcuts:
|
|
270
|
+
"""Test futures shortcut commands."""
|
|
271
|
+
|
|
272
|
+
@pytest.mark.slow
|
|
273
|
+
def test_futures_hist(self):
|
|
274
|
+
"""Shortcut: futures hist 热卷主连 --json (slow: fetches full history first)"""
|
|
275
|
+
data = run_json([
|
|
276
|
+
"futures", "hist", "热卷主连", "--limit", "5",
|
|
277
|
+
], timeout=300)
|
|
278
|
+
assert_dataframe_json(data)
|
|
279
|
+
|
|
280
|
+
@pytest.mark.slow
|
|
281
|
+
def test_futures_list(self):
|
|
282
|
+
"""Shortcut: futures list --json (slow: API often times out)"""
|
|
283
|
+
data = run_json(["futures", "list", "--limit", "5"], timeout=60)
|
|
284
|
+
assert_dataframe_json(data)
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
# ─── Bond Tests ──────────────────────────────────────────────────────────────
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
@pytest.mark.network
|
|
291
|
+
class TestBondCommands:
|
|
292
|
+
"""Test bond-related CLI commands from documentation."""
|
|
293
|
+
|
|
294
|
+
def test_bond_zh_cov(self):
|
|
295
|
+
"""可转债-概览表"""
|
|
296
|
+
data = run_json(["call", "bond_zh_cov", "--limit", "5"], timeout=60)
|
|
297
|
+
assert_dataframe_json(data)
|
|
298
|
+
|
|
299
|
+
def test_bond_cb_redeem_jsl(self):
|
|
300
|
+
"""集思录-可转债-强赎数据"""
|
|
301
|
+
data = run_json(["call", "bond_cb_redeem_jsl"], timeout=60)
|
|
302
|
+
assert_dataframe_json(data)
|
|
303
|
+
|
|
304
|
+
def test_bond_china_yield(self):
|
|
305
|
+
"""中国债券-收益率曲线"""
|
|
306
|
+
data = run_json([
|
|
307
|
+
"call", "bond_china_yield",
|
|
308
|
+
"--start_date", "20240101",
|
|
309
|
+
"--end_date", "20240110",
|
|
310
|
+
])
|
|
311
|
+
assert_dataframe_json(data)
|
|
312
|
+
|
|
313
|
+
def test_bond_zh_us_rate(self):
|
|
314
|
+
"""中美国债收益率"""
|
|
315
|
+
data = run_json(["call", "bond_zh_us_rate", "--start_date", "20240101"])
|
|
316
|
+
assert_dataframe_json(data)
|
|
317
|
+
|
|
318
|
+
def test_bond_spot_quote(self):
|
|
319
|
+
"""债券-做市报价"""
|
|
320
|
+
data = run_json(["call", "bond_spot_quote", "--limit", "5"], timeout=60)
|
|
321
|
+
assert_dataframe_json(data)
|
|
322
|
+
|
|
323
|
+
@pytest.mark.slow
|
|
324
|
+
def test_bond_cov_comparison(self):
|
|
325
|
+
"""可转债-对比表 (slow: API often disconnects)"""
|
|
326
|
+
data = run_json(["call", "bond_cov_comparison", "--limit", "5"], timeout=60)
|
|
327
|
+
assert_dataframe_json(data)
|
|
328
|
+
|
|
329
|
+
def test_bond_gb_zh_sina(self):
|
|
330
|
+
"""新浪-中国国债收益率 (10年期)"""
|
|
331
|
+
data = run_json([
|
|
332
|
+
"call", "bond_gb_zh_sina",
|
|
333
|
+
"--symbol", "中国10年期国债",
|
|
334
|
+
])
|
|
335
|
+
assert_dataframe_json(data)
|
|
336
|
+
|
|
337
|
+
def test_bond_gb_us_sina(self):
|
|
338
|
+
"""新浪-美国国债收益率 (10年期)"""
|
|
339
|
+
data = run_json([
|
|
340
|
+
"call", "bond_gb_us_sina",
|
|
341
|
+
"--symbol", "美国10年期国债",
|
|
342
|
+
])
|
|
343
|
+
assert_dataframe_json(data)
|
|
344
|
+
|
|
345
|
+
|
|
346
|
+
# ─── Bond Shortcut Tests ────────────────────────────────────────────────────
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
@pytest.mark.network
|
|
350
|
+
class TestBondShortcuts:
|
|
351
|
+
"""Test bond shortcut commands."""
|
|
352
|
+
|
|
353
|
+
def test_bond_convertible(self):
|
|
354
|
+
"""Shortcut: bond convertible --json"""
|
|
355
|
+
data = run_json(["bond", "convertible", "--limit", "5"], timeout=60)
|
|
356
|
+
assert_dataframe_json(data)
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
# ─── Macro Tests ─────────────────────────────────────────────────────────────
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
@pytest.mark.network
|
|
363
|
+
class TestMacroCommands:
|
|
364
|
+
"""Test macroeconomic CLI commands from documentation."""
|
|
365
|
+
|
|
366
|
+
def test_macro_china_gdp_yearly(self):
|
|
367
|
+
"""中国GDP年率"""
|
|
368
|
+
data = run_json(["call", "macro_china_gdp_yearly"])
|
|
369
|
+
assert_dataframe_json(data)
|
|
370
|
+
|
|
371
|
+
def test_macro_china_cpi_yearly(self):
|
|
372
|
+
"""中国CPI年率"""
|
|
373
|
+
data = run_json(["call", "macro_china_cpi_yearly"])
|
|
374
|
+
assert_dataframe_json(data)
|
|
375
|
+
|
|
376
|
+
def test_macro_china_cpi_monthly(self):
|
|
377
|
+
"""中国CPI月率"""
|
|
378
|
+
data = run_json(["call", "macro_china_cpi_monthly"])
|
|
379
|
+
assert_dataframe_json(data)
|
|
380
|
+
|
|
381
|
+
def test_macro_china_ppi_yearly(self):
|
|
382
|
+
"""中国PPI年率"""
|
|
383
|
+
data = run_json(["call", "macro_china_ppi_yearly"])
|
|
384
|
+
assert_dataframe_json(data)
|
|
385
|
+
|
|
386
|
+
def test_macro_china_pmi_yearly(self):
|
|
387
|
+
"""中国官方制造业PMI"""
|
|
388
|
+
data = run_json(["call", "macro_china_pmi_yearly"])
|
|
389
|
+
assert_dataframe_json(data)
|
|
390
|
+
|
|
391
|
+
def test_macro_china_lpr(self):
|
|
392
|
+
"""贷款市场报价利率 (LPR)"""
|
|
393
|
+
data = run_json(["call", "macro_china_lpr"])
|
|
394
|
+
assert_dataframe_json(data)
|
|
395
|
+
|
|
396
|
+
def test_macro_china_m2_yearly(self):
|
|
397
|
+
"""M2货币供应量同比"""
|
|
398
|
+
data = run_json(["call", "macro_china_m2_yearly"])
|
|
399
|
+
assert_dataframe_json(data)
|
|
400
|
+
|
|
401
|
+
def test_macro_china_exports_yoy(self):
|
|
402
|
+
"""中国出口额同比"""
|
|
403
|
+
data = run_json(["call", "macro_china_exports_yoy"])
|
|
404
|
+
assert_dataframe_json(data)
|
|
405
|
+
|
|
406
|
+
def test_macro_china_trade_balance(self):
|
|
407
|
+
"""中国贸易差额"""
|
|
408
|
+
data = run_json(["call", "macro_china_trade_balance"])
|
|
409
|
+
assert_dataframe_json(data)
|
|
410
|
+
|
|
411
|
+
def test_macro_china_lpi_index(self):
|
|
412
|
+
"""物流景气指数"""
|
|
413
|
+
data = run_json(["call", "macro_china_lpi_index"])
|
|
414
|
+
assert_dataframe_json(data)
|
|
415
|
+
|
|
416
|
+
def test_macro_shipping_bdi(self):
|
|
417
|
+
"""波罗的海干散货指数"""
|
|
418
|
+
data = run_json(["call", "macro_shipping_bdi"])
|
|
419
|
+
assert_dataframe_json(data)
|
|
420
|
+
|
|
421
|
+
def test_macro_china_shrzgm(self):
|
|
422
|
+
"""社会融资规模"""
|
|
423
|
+
data = run_json(["call", "macro_china_shrzgm"])
|
|
424
|
+
assert_dataframe_json(data)
|
|
425
|
+
|
|
426
|
+
def test_macro_cnbs(self):
|
|
427
|
+
"""中国宏观杠杆率"""
|
|
428
|
+
data = run_json(["call", "macro_cnbs"])
|
|
429
|
+
assert_dataframe_json(data)
|
|
430
|
+
|
|
431
|
+
def test_macro_china_urban_unemployment(self):
|
|
432
|
+
"""城镇调查失业率"""
|
|
433
|
+
data = run_json(["call", "macro_china_urban_unemployment"])
|
|
434
|
+
assert_dataframe_json(data)
|
|
435
|
+
|
|
436
|
+
def test_macro_china_cx_pmi_yearly(self):
|
|
437
|
+
"""财新制造业PMI"""
|
|
438
|
+
data = run_json(["call", "macro_china_cx_pmi_yearly"])
|
|
439
|
+
assert_dataframe_json(data)
|
|
440
|
+
|
|
441
|
+
|
|
442
|
+
# ─── Macro Shortcut Tests ───────────────────────────────────────────────────
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
@pytest.mark.network
|
|
446
|
+
class TestMacroShortcuts:
|
|
447
|
+
"""Test macro shortcut commands."""
|
|
448
|
+
|
|
449
|
+
def test_macro_gdp(self):
|
|
450
|
+
"""Shortcut: macro gdp --json"""
|
|
451
|
+
data = run_json(["macro", "gdp"])
|
|
452
|
+
assert_dataframe_json(data)
|
|
453
|
+
|
|
454
|
+
def test_macro_cpi(self):
|
|
455
|
+
"""Shortcut: macro cpi --json"""
|
|
456
|
+
data = run_json(["macro", "cpi"])
|
|
457
|
+
assert_dataframe_json(data)
|
|
458
|
+
|
|
459
|
+
def test_macro_cpi_yearly(self):
|
|
460
|
+
"""Shortcut: macro cpi --freq yearly --json"""
|
|
461
|
+
data = run_json(["macro", "cpi", "--freq", "yearly"])
|
|
462
|
+
assert_dataframe_json(data)
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
# ─── Forex Tests ─────────────────────────────────────────────────────────────
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
@pytest.mark.network
|
|
469
|
+
class TestForexCommands:
|
|
470
|
+
"""Test forex CLI commands."""
|
|
471
|
+
|
|
472
|
+
@pytest.mark.slow
|
|
473
|
+
def test_forex_spot_em(self):
|
|
474
|
+
"""外汇-实时行情 (slow: API often disconnects)"""
|
|
475
|
+
data = run_json(["call", "forex_spot_em", "--limit", "5"], timeout=60)
|
|
476
|
+
assert_dataframe_json(data)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
# ─── Forex Shortcut Tests ───────────────────────────────────────────────────
|
|
480
|
+
|
|
481
|
+
|
|
482
|
+
@pytest.mark.network
|
|
483
|
+
class TestForexShortcuts:
|
|
484
|
+
"""Test forex shortcut commands."""
|
|
485
|
+
|
|
486
|
+
@pytest.mark.slow
|
|
487
|
+
def test_forex_spot(self):
|
|
488
|
+
"""Shortcut: forex spot --json (slow: API often disconnects)"""
|
|
489
|
+
data = run_json(["forex", "spot", "--limit", "5"], timeout=60)
|
|
490
|
+
assert_dataframe_json(data)
|
|
491
|
+
|
|
492
|
+
|
|
493
|
+
# ─── Index Tests ─────────────────────────────────────────────────────────────
|
|
494
|
+
|
|
495
|
+
|
|
496
|
+
@pytest.mark.network
|
|
497
|
+
class TestIndexCommands:
|
|
498
|
+
"""Test index CLI commands."""
|
|
499
|
+
|
|
500
|
+
@pytest.mark.slow
|
|
501
|
+
def test_index_spot_global(self):
|
|
502
|
+
"""Shortcut: index spot --json (slow: API often disconnects)"""
|
|
503
|
+
data = run_json(["index", "spot", "--limit", "5"], timeout=60)
|
|
504
|
+
assert_dataframe_json(data)
|
|
505
|
+
|
|
506
|
+
|
|
507
|
+
# ─── News Tests ──────────────────────────────────────────────────────────────
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
@pytest.mark.network
|
|
511
|
+
class TestNewsCommands:
|
|
512
|
+
"""Test news CLI commands."""
|
|
513
|
+
|
|
514
|
+
def test_news_cctv(self):
|
|
515
|
+
"""央视新闻联播文字稿"""
|
|
516
|
+
data = run_json(["call", "news_cctv", "--date", "20240101"], timeout=120)
|
|
517
|
+
assert_dataframe_json(data)
|
|
518
|
+
assert "title" in data["columns"]
|
|
519
|
+
|
|
520
|
+
def test_news_economic_baidu(self):
|
|
521
|
+
"""百度经济数据日历"""
|
|
522
|
+
data = run_json(["call", "news_economic_baidu"], timeout=60)
|
|
523
|
+
assert_dataframe_json(data)
|
|
524
|
+
|
|
525
|
+
def test_stock_news_main_cx(self):
|
|
526
|
+
"""财新网-财经新闻"""
|
|
527
|
+
data = run_json(["call", "stock_news_main_cx", "--limit", "5"], timeout=60)
|
|
528
|
+
assert_dataframe_json(data)
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
# ─── Output Format Tests ────────────────────────────────────────────────────
|
|
532
|
+
|
|
533
|
+
|
|
534
|
+
@pytest.mark.network
|
|
535
|
+
class TestOutputFormats:
|
|
536
|
+
"""Test --json / --csv / --output work in various positions."""
|
|
537
|
+
|
|
538
|
+
def test_json_before_subcommand(self):
|
|
539
|
+
"""--json before subcommand: cli --json search stock"""
|
|
540
|
+
result = run_cli(["--json", "search", "stock_zh_a_hist"])
|
|
541
|
+
assert result.returncode == 0
|
|
542
|
+
data = json.loads(result.stdout)
|
|
543
|
+
assert data["count"] > 0
|
|
544
|
+
|
|
545
|
+
def test_json_after_subcommand(self):
|
|
546
|
+
"""--json after subcommand: cli search stock --json"""
|
|
547
|
+
result = run_cli(["search", "stock_zh_a_hist", "--json"])
|
|
548
|
+
assert result.returncode == 0
|
|
549
|
+
data = json.loads(result.stdout)
|
|
550
|
+
assert data["count"] > 0
|
|
551
|
+
|
|
552
|
+
def test_json_before_call_func(self):
|
|
553
|
+
"""--json between call and func_name: cli call --json macro_china_gdp_yearly"""
|
|
554
|
+
data = run_json(["call", "--json", "macro_china_gdp_yearly"])
|
|
555
|
+
assert_dataframe_json(data)
|
|
556
|
+
|
|
557
|
+
def test_json_after_call_params(self):
|
|
558
|
+
"""--json at end of call: cli call news_cctv --date 20240101 --json"""
|
|
559
|
+
data = run_json(["call", "news_cctv", "--date", "20240101"], timeout=120)
|
|
560
|
+
assert_dataframe_json(data)
|
|
561
|
+
|
|
562
|
+
def test_csv_output(self):
|
|
563
|
+
"""--csv output mode"""
|
|
564
|
+
result = run_cli(["--csv", "call", "macro_china_gdp_yearly"])
|
|
565
|
+
assert result.returncode == 0
|
|
566
|
+
lines = result.stdout.strip().split("\n")
|
|
567
|
+
assert len(lines) > 1, "CSV should have header + data rows"
|
|
568
|
+
|
|
569
|
+
def test_limit_flag(self):
|
|
570
|
+
"""--limit restricts output rows"""
|
|
571
|
+
data = run_json(["call", "macro_china_cpi_yearly", "--limit", "3"])
|
|
572
|
+
assert data["total_rows"] == 3
|
|
573
|
+
|
|
574
|
+
def test_output_to_csv_file(self):
|
|
575
|
+
"""--output saves to file"""
|
|
576
|
+
with tempfile.NamedTemporaryFile(suffix=".csv", delete=False) as f:
|
|
577
|
+
filepath = f.name
|
|
578
|
+
try:
|
|
579
|
+
result = run_cli([
|
|
580
|
+
"call", "macro_china_gdp_yearly",
|
|
581
|
+
"--output", filepath,
|
|
582
|
+
])
|
|
583
|
+
assert result.returncode == 0
|
|
584
|
+
assert os.path.exists(filepath)
|
|
585
|
+
assert os.path.getsize(filepath) > 0
|
|
586
|
+
with open(filepath) as f:
|
|
587
|
+
content = f.read()
|
|
588
|
+
assert len(content.strip().split("\n")) > 1
|
|
589
|
+
finally:
|
|
590
|
+
if os.path.exists(filepath):
|
|
591
|
+
os.unlink(filepath)
|
|
592
|
+
|
|
593
|
+
def test_output_to_json_file(self):
|
|
594
|
+
"""--output saves to .json file"""
|
|
595
|
+
with tempfile.NamedTemporaryFile(suffix=".json", delete=False) as f:
|
|
596
|
+
filepath = f.name
|
|
597
|
+
try:
|
|
598
|
+
result = run_cli([
|
|
599
|
+
"call", "macro_china_gdp_yearly",
|
|
600
|
+
"--output", filepath,
|
|
601
|
+
])
|
|
602
|
+
assert result.returncode == 0
|
|
603
|
+
assert os.path.exists(filepath)
|
|
604
|
+
with open(filepath) as f:
|
|
605
|
+
data = json.load(f)
|
|
606
|
+
assert isinstance(data, list) or isinstance(data, dict)
|
|
607
|
+
finally:
|
|
608
|
+
if os.path.exists(filepath):
|
|
609
|
+
os.unlink(filepath)
|
|
610
|
+
|
|
611
|
+
|
|
612
|
+
# ─── Shortcut + JSON Combined Tests ─────────────────────────────────────────
|
|
613
|
+
|
|
614
|
+
|
|
615
|
+
@pytest.mark.network
|
|
616
|
+
class TestShortcutJsonCombined:
|
|
617
|
+
"""Test that shortcuts work with --json in various positions."""
|
|
618
|
+
|
|
619
|
+
def test_stock_hist_json_after(self):
|
|
620
|
+
"""stock hist 000001 --json (--json after shortcut args)"""
|
|
621
|
+
data = run_json([
|
|
622
|
+
"stock", "hist", "000001",
|
|
623
|
+
"--start", "20240101", "--end", "20240110",
|
|
624
|
+
])
|
|
625
|
+
assert_dataframe_json(data)
|
|
626
|
+
|
|
627
|
+
def test_macro_gdp_json_after(self):
|
|
628
|
+
"""macro gdp --json"""
|
|
629
|
+
data = run_json(["macro", "gdp"])
|
|
630
|
+
assert_dataframe_json(data)
|
|
631
|
+
|
|
632
|
+
@pytest.mark.slow
|
|
633
|
+
def test_futures_hist_json_after(self):
|
|
634
|
+
"""futures hist 热卷主连 --json --limit 3 (slow: full history fetch)"""
|
|
635
|
+
data = run_json(["futures", "hist", "热卷主连", "--limit", "3"], timeout=300)
|
|
636
|
+
assert_dataframe_json(data, min_rows=3)
|