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.
@@ -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)