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,262 @@
1
+ """
2
+ End-to-end tests for AKShare CLI harness.
3
+
4
+ Tests the installed CLI via subprocess. Includes TestCLISubprocess with
5
+ _resolve_cli() for testing the installed command.
6
+ """
7
+
8
+ import json
9
+ import os
10
+ import shutil
11
+ import subprocess
12
+ import sys
13
+ import tempfile
14
+
15
+ import pytest
16
+
17
+
18
+ class TestCLISubprocess:
19
+ """E2E tests that invoke the CLI as a subprocess."""
20
+
21
+ @staticmethod
22
+ def _resolve_cli(name: str = "akshare-cli") -> str:
23
+ """
24
+ Resolve the CLI executable path.
25
+
26
+ If CLI_ANYTHING_FORCE_INSTALLED=1 is set, uses the system PATH.
27
+ Otherwise, falls back to the venv binary.
28
+ """
29
+ if os.environ.get("CLI_ANYTHING_FORCE_INSTALLED") == "1":
30
+ path = shutil.which(name)
31
+ if path:
32
+ return path
33
+ raise FileNotFoundError(
34
+ f"'{name}' not found in PATH. "
35
+ f"Install with: pip install -e ."
36
+ )
37
+
38
+ # Try venv first
39
+ venv_bin = os.path.join(
40
+ os.path.dirname(os.path.dirname(os.path.dirname(
41
+ os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
42
+ ))),
43
+ ".venv", "bin", name,
44
+ )
45
+ if os.path.isfile(venv_bin):
46
+ return venv_bin
47
+
48
+ # Try system PATH
49
+ path = shutil.which(name)
50
+ if path:
51
+ return path
52
+
53
+ # Fallback: run as module
54
+ return None
55
+
56
+ @classmethod
57
+ def _run_cli(cls, args: list, timeout: int = 30, env: dict = None) -> subprocess.CompletedProcess:
58
+ """Run CLI command and return the result."""
59
+ cli_path = cls._resolve_cli()
60
+
61
+ run_env = os.environ.copy()
62
+ if env:
63
+ run_env.update(env)
64
+
65
+ if cli_path:
66
+ cmd = [cli_path] + args
67
+ else:
68
+ cmd = [sys.executable, "-m", "akshare_cli.cli"] + args
69
+
70
+ return subprocess.run(
71
+ cmd,
72
+ capture_output=True,
73
+ text=True,
74
+ timeout=timeout,
75
+ env=run_env,
76
+ )
77
+
78
+ # ─── Basic CLI Tests ─────────────────────────────────────────────────
79
+
80
+ def test_help(self):
81
+ result = self._run_cli(["--help"])
82
+ assert result.returncode == 0
83
+ assert "AKShare CLI" in result.stdout
84
+ assert "Commands:" in result.stdout
85
+
86
+ def test_version_flag(self):
87
+ result = self._run_cli(["--version"])
88
+ assert result.returncode == 0
89
+ assert "0.1.0" in result.stdout
90
+
91
+ def test_version_command(self):
92
+ result = self._run_cli(["version"])
93
+ assert result.returncode == 0
94
+ assert "cli_harness_version" in result.stdout
95
+ assert "akshare_version" in result.stdout
96
+
97
+ # ─── Search Tests ────────────────────────────────────────────────────
98
+
99
+ def test_search(self):
100
+ result = self._run_cli(["search", "stock_zh_a"])
101
+ assert result.returncode == 0
102
+ assert "stock_zh_a_hist" in result.stdout
103
+
104
+ def test_search_json(self):
105
+ result = self._run_cli(["--json", "search", "stock_zh_a"])
106
+ assert result.returncode == 0
107
+ data = json.loads(result.stdout)
108
+ assert data["count"] > 0
109
+ assert "stock_zh_a_hist" in data["functions"]
110
+
111
+ def test_search_no_results(self):
112
+ result = self._run_cli(["search", "zzzznonexistent999"])
113
+ assert result.returncode == 0
114
+ assert "No functions found" in result.stdout
115
+
116
+ # ─── List Tests ──────────────────────────────────────────────────────
117
+
118
+ def test_list_domains(self):
119
+ result = self._run_cli(["list"])
120
+ assert result.returncode == 0
121
+ assert "stock" in result.stdout
122
+ assert "fund" in result.stdout
123
+ assert "functions" in result.stdout
124
+
125
+ def test_list_domain_stock(self):
126
+ result = self._run_cli(["list", "stock"])
127
+ assert result.returncode == 0
128
+ assert "stock_zh_a_hist" in result.stdout
129
+
130
+ def test_list_json(self):
131
+ result = self._run_cli(["--json", "list"])
132
+ assert result.returncode == 0
133
+ data = json.loads(result.stdout)
134
+ assert "total_functions" in data
135
+ assert data["total_functions"] > 500
136
+
137
+ # ─── Info Tests ──────────────────────────────────────────────────────
138
+
139
+ def test_info(self):
140
+ result = self._run_cli(["info", "stock_zh_a_hist"])
141
+ assert result.returncode == 0
142
+ assert "stock_zh_a_hist" in result.stdout
143
+ assert "--symbol" in result.stdout
144
+ assert "--period" in result.stdout
145
+
146
+ def test_info_json(self):
147
+ result = self._run_cli(["--json", "info", "stock_zh_a_hist"])
148
+ assert result.returncode == 0
149
+ data = json.loads(result.stdout)
150
+ assert data["name"] == "stock_zh_a_hist"
151
+ assert isinstance(data["params"], list)
152
+
153
+ def test_info_not_found(self):
154
+ result = self._run_cli(["info", "nonexistent_xyz"])
155
+ assert result.returncode != 0
156
+
157
+ # ─── Call Tests (with real network) ──────────────────────────────────
158
+
159
+ @pytest.mark.network
160
+ def test_call_with_data(self):
161
+ """Test calling a real akshare function (requires network)."""
162
+ result = self._run_cli(["call", "macro_china_gdp_yearly"], timeout=60)
163
+ assert result.returncode == 0
164
+ assert "rows" in result.stderr or len(result.stdout) > 50
165
+
166
+ @pytest.mark.network
167
+ def test_call_json_output(self):
168
+ """Test JSON output mode with real data."""
169
+ result = self._run_cli(["--json", "call", "macro_china_gdp_yearly"], timeout=60)
170
+ assert result.returncode == 0
171
+ data = json.loads(result.stdout)
172
+ assert "total_rows" in data
173
+ assert "data" in data
174
+ assert len(data["data"]) > 0
175
+
176
+ @pytest.mark.network
177
+ def test_call_csv_output(self):
178
+ """Test CSV output mode with real data."""
179
+ result = self._run_cli(["--csv", "call", "macro_china_gdp_yearly"], timeout=60)
180
+ assert result.returncode == 0
181
+ lines = result.stdout.strip().split("\n")
182
+ assert len(lines) > 1
183
+
184
+ def test_call_unknown_function(self):
185
+ result = self._run_cli(["call", "totally_nonexistent_xyz"])
186
+ assert result.returncode != 0
187
+ assert "not found" in result.stdout.lower() or "error" in result.stdout.lower() or "not found" in result.stderr.lower()
188
+
189
+ # ─── Subcommand Tests ────────────────────────────────────────────────
190
+
191
+ def test_stock_help(self):
192
+ result = self._run_cli(["stock", "--help"])
193
+ assert result.returncode == 0
194
+ assert "hist" in result.stdout
195
+ assert "spot" in result.stdout
196
+
197
+ def test_fund_help(self):
198
+ result = self._run_cli(["fund", "--help"])
199
+ assert result.returncode == 0
200
+ assert "etf" in result.stdout
201
+
202
+ def test_futures_help(self):
203
+ result = self._run_cli(["futures", "--help"])
204
+ assert result.returncode == 0
205
+ assert "hist" in result.stdout
206
+
207
+ def test_macro_help(self):
208
+ result = self._run_cli(["macro", "--help"])
209
+ assert result.returncode == 0
210
+ assert "gdp" in result.stdout
211
+
212
+ def test_forex_help(self):
213
+ result = self._run_cli(["forex", "--help"])
214
+ assert result.returncode == 0
215
+ assert "spot" in result.stdout
216
+ assert "hist" in result.stdout
217
+
218
+ # ─── Export Tests ────────────────────────────────────────────────────
219
+
220
+ @pytest.mark.network
221
+ def test_output_to_file(self):
222
+ """Test saving output directly to a file."""
223
+ with tempfile.NamedTemporaryFile(suffix=".csv", delete=False) as f:
224
+ filepath = f.name
225
+
226
+ try:
227
+ result = self._run_cli(
228
+ ["--output", filepath, "call", "macro_china_gdp_yearly"],
229
+ timeout=60,
230
+ )
231
+ assert result.returncode == 0
232
+ assert os.path.exists(filepath)
233
+ assert os.path.getsize(filepath) > 0
234
+ finally:
235
+ if os.path.exists(filepath):
236
+ os.unlink(filepath)
237
+
238
+ # ─── Workflow Tests ──────────────────────────────────────────────────
239
+
240
+ def test_workflow_search_then_info(self):
241
+ """Workflow: search for functions then get info on one."""
242
+ # Step 1: Search
243
+ result = self._run_cli(["search", "forex"])
244
+ assert result.returncode == 0
245
+ assert "forex_spot_em" in result.stdout
246
+
247
+ # Step 2: Get info
248
+ result = self._run_cli(["info", "forex_spot_em"])
249
+ assert result.returncode == 0
250
+ assert "forex_spot_em" in result.stdout
251
+
252
+ def test_workflow_json_pipeline(self):
253
+ """Workflow: JSON output for machine consumption."""
254
+ result = self._run_cli(["--json", "list"])
255
+ assert result.returncode == 0
256
+ data = json.loads(result.stdout)
257
+ assert isinstance(data["total_functions"], int)
258
+
259
+ result = self._run_cli(["--json", "search", "macro_china"])
260
+ assert result.returncode == 0
261
+ data = json.loads(result.stdout)
262
+ assert len(data["functions"]) > 0
@@ -0,0 +1 @@
1
+ """Utilities package for AKShare CLI harness."""
@@ -0,0 +1,62 @@
1
+ """
2
+ Formatting and display utilities for AKShare CLI harness.
3
+ """
4
+
5
+ import sys
6
+ from typing import Optional
7
+
8
+ import pandas as pd
9
+
10
+ from akshare_cli.core.export import format_csv, format_json, format_table
11
+
12
+
13
+ def display_result(
14
+ df: pd.DataFrame,
15
+ output_format: str = "table",
16
+ max_rows: Optional[int] = None,
17
+ show_index: bool = False,
18
+ output_file: Optional[str] = None,
19
+ ) -> str:
20
+ """
21
+ Format and display a DataFrame result.
22
+
23
+ Args:
24
+ df: The DataFrame to display.
25
+ output_format: One of "table", "json", "csv".
26
+ max_rows: Maximum rows to display.
27
+ show_index: Whether to show row index.
28
+ output_file: If set, write output to this file path.
29
+
30
+ Returns:
31
+ The formatted string.
32
+ """
33
+ if output_format == "json":
34
+ output = format_json(df, max_rows=max_rows)
35
+ elif output_format == "csv":
36
+ output = format_csv(df, max_rows=max_rows)
37
+ else:
38
+ output = format_table(df, max_rows=max_rows, show_index=show_index)
39
+
40
+ if output_file:
41
+ from akshare_cli.core.export import auto_export
42
+ auto_export(df, output_file)
43
+ return f"Saved to {output_file}"
44
+
45
+ return output
46
+
47
+
48
+ def print_error(message: str) -> None:
49
+ """Print an error message to stderr."""
50
+ print(f"Error: {message}", file=sys.stderr)
51
+
52
+
53
+ def print_info(message: str) -> None:
54
+ """Print an informational message."""
55
+ print(message)
56
+
57
+
58
+ def truncate_string(s: str, max_len: int = 60) -> str:
59
+ """Truncate a string with ellipsis if it exceeds max_len."""
60
+ if len(s) <= max_len:
61
+ return s
62
+ return s[: max_len - 3] + "..."