alpha-dc-client 0.1.23__py2.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.
- alpha_dc_client-0.1.23.dist-info/METADATA +6 -0
- alpha_dc_client-0.1.23.dist-info/RECORD +11 -0
- alpha_dc_client-0.1.23.dist-info/WHEEL +5 -0
- dc_client/__init__.py +13 -0
- dc_client/base.py +69 -0
- dc_client/client.py +221 -0
- dc_client/client.pyi +75 -0
- dc_client/exceptions.py +26 -0
- dc_client/run_tests.py +294 -0
- dc_client/tests/__init__.py +0 -0
- dc_client/tests/base.py +123 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
dc_client/__init__.py,sha256=kkr3Y8U6OWF2oWYYjLF0HRJ2pnLnoALjzyGJIBWsSSU,338
|
|
2
|
+
dc_client/base.py,sha256=k5X3-kMQq7ULH_N1NVloI0ji8Ks6ZPifVxZ_LaVcEx4,2584
|
|
3
|
+
dc_client/client.py,sha256=oDvvtp1CV1hyRE6-8T85r13jfCGxZfOt6gj46yJ0LMI,7463
|
|
4
|
+
dc_client/client.pyi,sha256=q7C7TeS7GUUylMOxxojvKt7-6d-yNogIvCR4CQB1Go8,3460
|
|
5
|
+
dc_client/exceptions.py,sha256=AHt-PVeBaly7-ehPrMRJlZrC6Ew_o1nuX_5gxcJUwaY,927
|
|
6
|
+
dc_client/run_tests.py,sha256=QGPDge-ZLUd9yNZxlQgX-n2LsgHwT72myZSkREyIPKw,10831
|
|
7
|
+
dc_client/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
8
|
+
dc_client/tests/base.py,sha256=p4txw8eWvD2-JnMOgADGlFgWLOTcxznIcKz7RzhuQ3U,5535
|
|
9
|
+
alpha_dc_client-0.1.23.dist-info/METADATA,sha256=S8LqqCS5nOZIeQ1DQlQe38HyLy2YTqKB0d059N7czVc,175
|
|
10
|
+
alpha_dc_client-0.1.23.dist-info/WHEEL,sha256=aha0VrrYvgDJ3Xxl3db_g_MDIW-ZexDdrc_m-Hk8YY4,105
|
|
11
|
+
alpha_dc_client-0.1.23.dist-info/RECORD,,
|
dc_client/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from .exceptions import APIError, AuthenticationError, NotFoundError, InvalidRequestError
|
|
2
|
+
from .client import DataApi, init, PageDataFrame, DatacenterAPIError
|
|
3
|
+
|
|
4
|
+
__all__ = [
|
|
5
|
+
"DataApi",
|
|
6
|
+
"init",
|
|
7
|
+
"PageDataFrame",
|
|
8
|
+
"DatacenterAPIError",
|
|
9
|
+
"APIError",
|
|
10
|
+
"AuthenticationError",
|
|
11
|
+
"NotFoundError",
|
|
12
|
+
"InvalidRequestError",
|
|
13
|
+
]
|
dc_client/base.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
from typing import Optional, Dict, Any
|
|
3
|
+
|
|
4
|
+
from .exceptions import APIError, AuthenticationError, NotFoundError, InvalidRequestError
|
|
5
|
+
|
|
6
|
+
class BaseClient:
|
|
7
|
+
"""
|
|
8
|
+
A base client for making requests to the Datacenter API.
|
|
9
|
+
"""
|
|
10
|
+
def __init__(self, base_url: str, token: Optional[str] = None, timeout: int = 30):
|
|
11
|
+
"""
|
|
12
|
+
Initializes the API client.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
base_url: The base URL for the API, e.g., "http://localhost:10000".
|
|
16
|
+
token: An optional token for authentication.
|
|
17
|
+
timeout: The request timeout in seconds.
|
|
18
|
+
"""
|
|
19
|
+
self._base_url = base_url.rstrip('/')
|
|
20
|
+
self._token = token
|
|
21
|
+
self._timeout = timeout
|
|
22
|
+
|
|
23
|
+
headers = {"Content-Type": "application/json"}
|
|
24
|
+
if self._token:
|
|
25
|
+
headers["X-API-Key"] = self._token
|
|
26
|
+
|
|
27
|
+
self._http_client = httpx.Client(base_url=self._base_url, headers=headers, timeout=self._timeout)
|
|
28
|
+
|
|
29
|
+
def _request(self, method: str, endpoint: str, params: Optional[Dict[str, Any]] = None, json: Optional[Dict[str, Any]] = None) -> Any:
|
|
30
|
+
"""
|
|
31
|
+
Makes an HTTP request and handles potential errors.
|
|
32
|
+
"""
|
|
33
|
+
try:
|
|
34
|
+
response = self._http_client.request(method, endpoint, params=params, json=json)
|
|
35
|
+
response.raise_for_status()
|
|
36
|
+
|
|
37
|
+
response_data = response.json()
|
|
38
|
+
if response_data.get("status") == "success":
|
|
39
|
+
return response_data
|
|
40
|
+
|
|
41
|
+
raise APIError(f"API returned non-success status: {response_data.get('status', 'N/A')}", status_code=response.status_code)
|
|
42
|
+
|
|
43
|
+
except httpx.HTTPStatusError as e:
|
|
44
|
+
status_code = e.response.status_code
|
|
45
|
+
try:
|
|
46
|
+
detail = e.response.json().get("detail", e.response.text)
|
|
47
|
+
except Exception:
|
|
48
|
+
detail = e.response.text
|
|
49
|
+
|
|
50
|
+
if status_code == 404:
|
|
51
|
+
raise NotFoundError(detail) from e
|
|
52
|
+
elif status_code in [401, 403]:
|
|
53
|
+
raise AuthenticationError(detail) from e
|
|
54
|
+
elif status_code == 400:
|
|
55
|
+
raise InvalidRequestError(detail) from e
|
|
56
|
+
else:
|
|
57
|
+
raise APIError(f"HTTP Error: {detail}", status_code=status_code) from e
|
|
58
|
+
except httpx.RequestError as e:
|
|
59
|
+
raise APIError(f"Request failed: {e}") from e
|
|
60
|
+
|
|
61
|
+
def close(self):
|
|
62
|
+
"""Closes the underlying HTTP client."""
|
|
63
|
+
self._http_client.close()
|
|
64
|
+
|
|
65
|
+
def __enter__(self):
|
|
66
|
+
return self
|
|
67
|
+
|
|
68
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
69
|
+
self.close()
|
dc_client/client.py
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
"""
|
|
2
|
+
通用客户端
|
|
3
|
+
提供简洁的数据查询接口
|
|
4
|
+
"""
|
|
5
|
+
import pandas as pd
|
|
6
|
+
import json
|
|
7
|
+
import requests
|
|
8
|
+
from functools import partial
|
|
9
|
+
from dc_common.schemas import ResultType
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class PageDataFrame(pd.DataFrame):
|
|
13
|
+
"""
|
|
14
|
+
带分页信息的DataFrame
|
|
15
|
+
|
|
16
|
+
继承自pandas.DataFrame,添加分页相关的属性和方法
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
_metadata = ['_pagination']
|
|
20
|
+
|
|
21
|
+
def __init__(self, data=None, index=None, columns=None, dtype=None, copy=False, pagination=None):
|
|
22
|
+
super().__init__(data=data, index=index, columns=columns, dtype=dtype, copy=copy)
|
|
23
|
+
self._pagination = pagination or {}
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def pagination(self):
|
|
27
|
+
"""获取分页信息"""
|
|
28
|
+
return self._pagination
|
|
29
|
+
|
|
30
|
+
@property
|
|
31
|
+
def has_pagination(self):
|
|
32
|
+
"""是否有分页信息"""
|
|
33
|
+
return bool(self._pagination)
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def current_page(self):
|
|
37
|
+
"""当前页码"""
|
|
38
|
+
return self._pagination.get('page', 1)
|
|
39
|
+
|
|
40
|
+
@property
|
|
41
|
+
def page_size(self):
|
|
42
|
+
"""每页大小"""
|
|
43
|
+
return self._pagination.get('page_size', len(self))
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def total_count(self):
|
|
47
|
+
"""总记录数"""
|
|
48
|
+
return self._pagination.get('total', len(self))
|
|
49
|
+
|
|
50
|
+
@property
|
|
51
|
+
def total_pages(self):
|
|
52
|
+
"""总页数"""
|
|
53
|
+
total = self.total_count
|
|
54
|
+
size = self.page_size
|
|
55
|
+
return (total + size - 1) // size if size > 0 else 1
|
|
56
|
+
|
|
57
|
+
def __repr__(self):
|
|
58
|
+
base_repr = super().__repr__()
|
|
59
|
+
if self.has_pagination:
|
|
60
|
+
pagination_info = f"Page {self.current_page}/{self.total_pages} (Total: {self.total_count})"
|
|
61
|
+
return f"{base_repr}\n[Pagination: {pagination_info}]"
|
|
62
|
+
return base_repr
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class DatacenterAPIError(Exception):
|
|
66
|
+
"""数据中心API异常类"""
|
|
67
|
+
|
|
68
|
+
def __init__(self, code: int, message: str):
|
|
69
|
+
self.code = code
|
|
70
|
+
self.message = message
|
|
71
|
+
super().__init__(f"API错误 (code: {code}): {message}")
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class DataApi:
|
|
75
|
+
"""
|
|
76
|
+
简洁通用数据API客户端
|
|
77
|
+
|
|
78
|
+
使用方式:
|
|
79
|
+
```python
|
|
80
|
+
import dc_client as dc
|
|
81
|
+
|
|
82
|
+
# 初始化客户端
|
|
83
|
+
client = dc.api(token='your_token', base_url='http://localhost:10000')
|
|
84
|
+
|
|
85
|
+
# 查询HSGT分页数据
|
|
86
|
+
df = client.hsgt_page_list(page=1, page_size=20)
|
|
87
|
+
|
|
88
|
+
# 查询指定字段的HSGT分页数据
|
|
89
|
+
df = client.hsgt_page_list(page=1, page_size=10, fields='trade_date,stock_code,stock_name,hold_market_cap')
|
|
90
|
+
|
|
91
|
+
# 按股票代码查询HSGT数据
|
|
92
|
+
df = client.hsgt_by_stock(stock_code='00700.HK', limit=50)
|
|
93
|
+
|
|
94
|
+
# 按股票代码查询并指定字段
|
|
95
|
+
df = client.hsgt_by_stock(stock_code='00700.HK', limit=10, fields='trade_date,stock_code,stock_name,hold_market_cap')
|
|
96
|
+
```
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
def __init__(self, token: str, base_url: str = '', local: bool = False, timeout: int = 30):
|
|
100
|
+
"""
|
|
101
|
+
初始化客户端
|
|
102
|
+
|
|
103
|
+
Args:
|
|
104
|
+
token: API认证token
|
|
105
|
+
base_url: API服务基础URL,为空时根据local参数使用默认值
|
|
106
|
+
local: 是否为本地环境,为True时默认使用http://localhost:10000
|
|
107
|
+
timeout: 请求超时时间
|
|
108
|
+
"""
|
|
109
|
+
self.__token = token
|
|
110
|
+
|
|
111
|
+
# 处理base_url逻辑
|
|
112
|
+
if not base_url:
|
|
113
|
+
base_url = 'http://localhost:10000' if local else 'https://data.alphaaidig.com'
|
|
114
|
+
|
|
115
|
+
# base_url.rstrip('/') 的作用是移除URL末尾的斜杠
|
|
116
|
+
# 例如: 'http://localhost:10000/' -> 'http://localhost:10000'
|
|
117
|
+
# 'http://localhost:10000' -> 'http://localhost:10000'
|
|
118
|
+
# 这样可以确保拼接API路径时不会出现双斜杠
|
|
119
|
+
self.__base_url = base_url.rstrip('/')
|
|
120
|
+
self.__timeout = timeout
|
|
121
|
+
self.__dataapi_url = f"{self.__base_url}/api/v1/dataapi"
|
|
122
|
+
|
|
123
|
+
def query(self, api_name: str, fields: str = '', **kwargs) -> pd.DataFrame:
|
|
124
|
+
"""
|
|
125
|
+
通用查询方法,与tushare的query方法完全一致
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
api_name: API名称,如'hsgt_south_fund'
|
|
129
|
+
fields: 需要返回的字段,逗号分隔,如'trade_date,stock_code,stock_name'
|
|
130
|
+
**kwargs: 查询参数
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
pd.DataFrame: 查询结果
|
|
134
|
+
"""
|
|
135
|
+
# 扁平化参数格式,与API期望格式一致
|
|
136
|
+
req_params = kwargs.copy()
|
|
137
|
+
req_params['fields'] = fields
|
|
138
|
+
|
|
139
|
+
try:
|
|
140
|
+
# 发送HTTP请求
|
|
141
|
+
url = f"{self.__dataapi_url}/{api_name}"
|
|
142
|
+
headers = {
|
|
143
|
+
'X-API-Key': self.__token,
|
|
144
|
+
'Content-Type': 'application/json'
|
|
145
|
+
}
|
|
146
|
+
res = requests.post(url, json=req_params, headers=headers, timeout=self.__timeout)
|
|
147
|
+
|
|
148
|
+
if res.status_code != 200:
|
|
149
|
+
raise Exception(f"HTTP请求失败,状态码: {res.status_code}")
|
|
150
|
+
|
|
151
|
+
result = res.json()
|
|
152
|
+
|
|
153
|
+
# 检查响应状态
|
|
154
|
+
if result.get('code') != 0:
|
|
155
|
+
raise DatacenterAPIError(
|
|
156
|
+
code=result.get('code', -1),
|
|
157
|
+
message=result.get('msg', '未知错误')
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# 解析数据
|
|
161
|
+
data = result.get('data', {})
|
|
162
|
+
if not data:
|
|
163
|
+
return pd.DataFrame()
|
|
164
|
+
|
|
165
|
+
# 根据结果类型转换为DataFrame
|
|
166
|
+
result_type = ResultType(result.get('result_type', 'table'))
|
|
167
|
+
|
|
168
|
+
if result_type == ResultType.DICT:
|
|
169
|
+
# 字典结果,转换为单行DataFrame
|
|
170
|
+
return pd.DataFrame([data])
|
|
171
|
+
elif result_type == ResultType.SINGLE:
|
|
172
|
+
# 单条记录,转换为单行DataFrame
|
|
173
|
+
return pd.DataFrame([data])
|
|
174
|
+
elif result_type == ResultType.PAGE:
|
|
175
|
+
# 分页结果,转换为PageDataFrame并保留分页信息
|
|
176
|
+
items = data.get('items', [])
|
|
177
|
+
if not items:
|
|
178
|
+
return pd.DataFrame()
|
|
179
|
+
pagination_info = result.get('pagination', {})
|
|
180
|
+
return PageDataFrame(items, pagination=pagination_info)
|
|
181
|
+
else:
|
|
182
|
+
# 默认表格结果
|
|
183
|
+
items = data.get('items', [])
|
|
184
|
+
if not items:
|
|
185
|
+
return pd.DataFrame()
|
|
186
|
+
return pd.DataFrame(items)
|
|
187
|
+
|
|
188
|
+
except requests.exceptions.Timeout:
|
|
189
|
+
raise Exception(f"请求超时: {self.__timeout}秒")
|
|
190
|
+
except requests.exceptions.ConnectionError:
|
|
191
|
+
raise Exception(f"无法连接到服务器: {self.__base_url}")
|
|
192
|
+
except json.JSONDecodeError:
|
|
193
|
+
raise Exception("响应数据格式错误")
|
|
194
|
+
except Exception as e:
|
|
195
|
+
raise Exception(f"查询失败: {str(e)}")
|
|
196
|
+
|
|
197
|
+
def __getattr__(self, name: str):
|
|
198
|
+
"""
|
|
199
|
+
魔术方法,动态创建API方法
|
|
200
|
+
例如:pro.hsgt_south_fund() 等价于 pro.query('hsgt_south_fund')
|
|
201
|
+
"""
|
|
202
|
+
return partial(self.query, name)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def init(token: str = '', base_url: str = '', local: bool = False, timeout: int = 30) -> DataApi:
|
|
206
|
+
"""
|
|
207
|
+
初始化API客户端
|
|
208
|
+
|
|
209
|
+
Args:
|
|
210
|
+
token: API认证token,必传参数
|
|
211
|
+
base_url: API服务基础URL,非必填,为空时默认是https://data.alphaaidig.com,如果是local=true则使用本地url
|
|
212
|
+
local: 是否为本地环境,非必填,默认为False,为True时默认使用http://localhost:10000
|
|
213
|
+
timeout: 请求超时时间,非必填
|
|
214
|
+
|
|
215
|
+
Returns:
|
|
216
|
+
DataApi: API客户端实例
|
|
217
|
+
"""
|
|
218
|
+
if not token:
|
|
219
|
+
raise ValueError('token不能为空')
|
|
220
|
+
|
|
221
|
+
return DataApi(token=token, base_url=base_url, local=local, timeout=timeout)
|
dc_client/client.pyi
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DataCenter Client 类型存根文件
|
|
3
|
+
为IDE提供类型提示和代码补全支持
|
|
4
|
+
"""
|
|
5
|
+
from typing import Any, Dict, Optional, Union
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
class PageDataFrame(pd.DataFrame):
|
|
9
|
+
"""带分页信息的DataFrame"""
|
|
10
|
+
_pagination: Dict[str, Any]
|
|
11
|
+
|
|
12
|
+
@property
|
|
13
|
+
def pagination(self) -> Dict[str, Any]: ...
|
|
14
|
+
@property
|
|
15
|
+
def has_pagination(self) -> bool: ...
|
|
16
|
+
@property
|
|
17
|
+
def current_page(self) -> int: ...
|
|
18
|
+
@property
|
|
19
|
+
def page_size(self) -> int: ...
|
|
20
|
+
@property
|
|
21
|
+
def total_count(self) -> int: ...
|
|
22
|
+
@property
|
|
23
|
+
def total_pages(self) -> int: ...
|
|
24
|
+
|
|
25
|
+
class DatacenterAPIError(Exception):
|
|
26
|
+
"""数据中心API异常类"""
|
|
27
|
+
code: int
|
|
28
|
+
message: str
|
|
29
|
+
|
|
30
|
+
class DataApi:
|
|
31
|
+
"""通用数据API客户端"""
|
|
32
|
+
__token: str
|
|
33
|
+
__base_url: str
|
|
34
|
+
__timeout: int
|
|
35
|
+
__dataapi_url: str
|
|
36
|
+
|
|
37
|
+
def __init__(self, token: str, base_url: str = '', local: bool = False, timeout: int = 30) -> None: ...
|
|
38
|
+
|
|
39
|
+
def query(self, api_name: str, fields: str = '', **kwargs) -> Union[pd.DataFrame, PageDataFrame]: ...
|
|
40
|
+
|
|
41
|
+
# HSGT 相关接口
|
|
42
|
+
def hsgt_fund_page_list(self, page: int = 1, page_size: int = 20, **kwargs) -> PageDataFrame: ...
|
|
43
|
+
def hsgt_north_fundxx(self, **kwargs) -> pd.DataFrame: ...
|
|
44
|
+
def hsgt_south_fundxx(self, **kwargs) -> pd.DataFrame: ...
|
|
45
|
+
|
|
46
|
+
# A股相关接口
|
|
47
|
+
def a_stock_page_list(self, page: int = 1, page_size: int = 20, **kwargs) -> PageDataFrame: ...
|
|
48
|
+
def a_stock_list(self, limit: int = 50, **kwargs) -> pd.DataFrame: ...
|
|
49
|
+
|
|
50
|
+
# 港股相关接口
|
|
51
|
+
def hk_stock_page_list(self, page: int = 1, page_size: int = 20, **kwargs) -> PageDataFrame: ...
|
|
52
|
+
def hk_stock_list(self, limit: int = 50, **kwargs) -> pd.DataFrame: ...
|
|
53
|
+
|
|
54
|
+
# 行业相关接口
|
|
55
|
+
def hs_industry_page_list(self, page: int = 1, page_size: int = 20, **kwargs) -> PageDataFrame: ...
|
|
56
|
+
def hs_industry_list(self, limit: int = 50, **kwargs) -> pd.DataFrame: ...
|
|
57
|
+
def hs_industry_company_page_list(self, page: int = 1, page_size: int = 20, **kwargs) -> PageDataFrame: ...
|
|
58
|
+
def hs_industry_company_list(self, limit: int = 50, **kwargs) -> pd.DataFrame: ...
|
|
59
|
+
def sw_industry_page_list(self, page: int = 1, page_size: int = 20, **kwargs) -> PageDataFrame: ...
|
|
60
|
+
def sw_industry_list(self, limit: int = 50, **kwargs) -> pd.DataFrame: ...
|
|
61
|
+
def sw_industry_company_page_list(self, page: int = 1, page_size: int = 20, level_type: str = 'level1', **kwargs) -> PageDataFrame: ...
|
|
62
|
+
def sw_industry_company_list(self, limit: int = 50, level_type: str = 'level1', **kwargs) -> pd.DataFrame: ...
|
|
63
|
+
|
|
64
|
+
# 融资融券相关接口
|
|
65
|
+
def margin_account_page_list(self, page: int = 1, page_size: int = 20, **kwargs) -> PageDataFrame: ...
|
|
66
|
+
def margin_account_list(self, limit: int = 50, **kwargs) -> pd.DataFrame: ...
|
|
67
|
+
def margin_analysis_page_list(self, page: int = 1, page_size: int = 20, **kwargs) -> PageDataFrame: ...
|
|
68
|
+
def margin_analysis_list(self, limit: int = 50, **kwargs) -> pd.DataFrame: ...
|
|
69
|
+
def margin_detail_page_list_by_date(self, page: int = 1, page_size: int = 20, **kwargs) -> PageDataFrame: ...
|
|
70
|
+
def margin_detail_page_list_by_stock(self, page: int = 1, page_size: int = 20, **kwargs) -> PageDataFrame: ...
|
|
71
|
+
def margin_detail_list_by_stock(self, stock_code: str, limit: int = 50, **kwargs) -> pd.DataFrame: ...
|
|
72
|
+
|
|
73
|
+
def init(token: str = '', base_url: str = '', local: bool = False, timeout: int = 30) -> DataApi: ...
|
|
74
|
+
|
|
75
|
+
__all__ = ["DataApi", "init", "PageDataFrame", "DatacenterAPIError"]
|
dc_client/exceptions.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Custom exceptions for the Datacenter API client.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
class APIError(Exception):
|
|
8
|
+
"""Base class for all API-related errors."""
|
|
9
|
+
def __init__(self, message: str, status_code: Optional[int] = None):
|
|
10
|
+
self.status_code = status_code
|
|
11
|
+
super().__init__(f"API Error (Status: {status_code}): {message}")
|
|
12
|
+
|
|
13
|
+
class AuthenticationError(APIError):
|
|
14
|
+
"""Raised for authentication errors (401, 403)."""
|
|
15
|
+
def __init__(self, message: str = "Authentication failed"):
|
|
16
|
+
super().__init__(message, status_code=401)
|
|
17
|
+
|
|
18
|
+
class NotFoundError(APIError):
|
|
19
|
+
"""Raised when a resource is not found (404)."""
|
|
20
|
+
def __init__(self, message: str = "Resource not found"):
|
|
21
|
+
super().__init__(message, status_code=404)
|
|
22
|
+
|
|
23
|
+
class InvalidRequestError(APIError):
|
|
24
|
+
"""Raised for client-side errors (400)."""
|
|
25
|
+
def __init__(self, message: str):
|
|
26
|
+
super().__init__(message, status_code=400)
|
dc_client/run_tests.py
ADDED
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
DataCenter Client 测试运行脚本
|
|
4
|
+
Handler模式迁移后的简化版本
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import sys
|
|
8
|
+
import os
|
|
9
|
+
import pandas as pd
|
|
10
|
+
|
|
11
|
+
# 添加当前目录到 Python 路径
|
|
12
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
13
|
+
parent_dir = os.path.dirname(current_dir)
|
|
14
|
+
sys.path.insert(0, parent_dir)
|
|
15
|
+
|
|
16
|
+
try:
|
|
17
|
+
import dc_client as dc
|
|
18
|
+
from dc_client import DataApi, PageDataFrame, DatacenterAPIError
|
|
19
|
+
except ImportError as e:
|
|
20
|
+
print(f"❌ 导入错误: {e}")
|
|
21
|
+
print("请确保已正确安装依赖: uv sync")
|
|
22
|
+
sys.exit(1)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def test_client_initialization():
|
|
26
|
+
"""测试客户端初始化功能"""
|
|
27
|
+
print("\n" + "=" * 50)
|
|
28
|
+
print("🧪 测试 1: 客户端初始化功能")
|
|
29
|
+
print("=" * 50)
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
# 测试使用 local=True 初始化
|
|
33
|
+
print("✅ 测试 local=True 初始化...")
|
|
34
|
+
client = dc.init(token='test_token', local=True)
|
|
35
|
+
assert client._DataApi__base_url == 'http://localhost:10000'
|
|
36
|
+
assert client._DataApi__token == 'test_token'
|
|
37
|
+
print(" ✓ local 参数工作正常")
|
|
38
|
+
|
|
39
|
+
# 测试使用显式 base_url 初始化
|
|
40
|
+
print("✅ 测试显式 base_url 初始化...")
|
|
41
|
+
client2 = dc.init(token='test_token', base_url='https://api.example.com')
|
|
42
|
+
assert client2._DataApi__base_url == 'https://api.example.com'
|
|
43
|
+
print(" ✓ base_url 参数工作正常")
|
|
44
|
+
|
|
45
|
+
# 测试默认值(非本地)
|
|
46
|
+
print("✅ 测试默认值初始化...")
|
|
47
|
+
client3 = dc.init(token='test_token')
|
|
48
|
+
assert client3._DataApi__base_url == 'https://data.alphaaidig.com'
|
|
49
|
+
print(" ✓ 默认生产环境URL工作正常")
|
|
50
|
+
|
|
51
|
+
# 测试空token错误
|
|
52
|
+
print("✅ 测试空token错误处理...")
|
|
53
|
+
try:
|
|
54
|
+
dc.init(token='')
|
|
55
|
+
assert False, "应该抛出ValueError"
|
|
56
|
+
except ValueError as e:
|
|
57
|
+
assert 'token不能为空' in str(e)
|
|
58
|
+
print(" ✓ 空token错误处理正常")
|
|
59
|
+
|
|
60
|
+
print("\n🎉 客户端初始化测试全部通过!")
|
|
61
|
+
return True
|
|
62
|
+
|
|
63
|
+
except Exception as e:
|
|
64
|
+
print(f"\n❌ 客户端初始化测试失败: {e}")
|
|
65
|
+
import traceback
|
|
66
|
+
traceback.print_exc()
|
|
67
|
+
return False
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def test_dynamic_method_creation():
|
|
71
|
+
"""测试动态方法创建功能"""
|
|
72
|
+
print("\n" + "=" * 50)
|
|
73
|
+
print("🧪 测试 2: 动态方法创建功能")
|
|
74
|
+
print("=" * 50)
|
|
75
|
+
|
|
76
|
+
try:
|
|
77
|
+
# 创建客户端(使用模拟token)
|
|
78
|
+
client = dc.init(token='test_token', local=True, timeout=5)
|
|
79
|
+
|
|
80
|
+
# 测试动态方法是否可创建
|
|
81
|
+
print("✅ 测试动态方法创建...")
|
|
82
|
+
dynamic_method = getattr(client, 'hsgt_fund_page_list', None)
|
|
83
|
+
assert dynamic_method is not None, "动态方法应该被创建"
|
|
84
|
+
print(" ✓ hsgt_fund_page_list 方法创建成功")
|
|
85
|
+
|
|
86
|
+
# 测试其他动态方法
|
|
87
|
+
methods_to_test = [
|
|
88
|
+
'margin_account_page_list',
|
|
89
|
+
'a_stock_page_list',
|
|
90
|
+
'hk_stock_page_list',
|
|
91
|
+
'hs_industry_page_list'
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
for method_name in methods_to_test:
|
|
95
|
+
method = getattr(client, method_name, None)
|
|
96
|
+
assert method is not None, f"{method_name} 方法应该被创建"
|
|
97
|
+
print(f" ✓ {len(methods_to_test)} 个动态方法创建成功")
|
|
98
|
+
|
|
99
|
+
# 测试方法调用(会失败,但验证方法存在)
|
|
100
|
+
print("✅ 测试动态方法调用...")
|
|
101
|
+
try:
|
|
102
|
+
# 这个调用会因为服务未运行或token无效而失败,但验证了方法存在
|
|
103
|
+
client.hsgt_fund_page_list(page=1, page_size=1)
|
|
104
|
+
except Exception as e:
|
|
105
|
+
# 期望的异常,说明方法可以被调用
|
|
106
|
+
print(f" ✓ 动态方法调用正常 (预期异常: {type(e).__name__})")
|
|
107
|
+
|
|
108
|
+
# 测试query方法
|
|
109
|
+
print("✅ 测试query方法...")
|
|
110
|
+
try:
|
|
111
|
+
client.query('test_handler', param1='value1')
|
|
112
|
+
except Exception as e:
|
|
113
|
+
# 期望的异常
|
|
114
|
+
print(f" ✓ query方法调用正常 (预期异常: {type(e).__name__})")
|
|
115
|
+
|
|
116
|
+
print("\n🎉 动态方法创建测试全部通过!")
|
|
117
|
+
return True
|
|
118
|
+
|
|
119
|
+
except Exception as e:
|
|
120
|
+
print(f"\n❌ 动态方法创建测试失败: {e}")
|
|
121
|
+
import traceback
|
|
122
|
+
traceback.print_exc()
|
|
123
|
+
return False
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def test_real_api_call():
|
|
127
|
+
"""测试真实API调用功能"""
|
|
128
|
+
print("\n" + "=" * 50)
|
|
129
|
+
print("🧪 测试 3: 真实API调用功能")
|
|
130
|
+
print("=" * 50)
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
# 创建客户端(使用测试token)
|
|
134
|
+
client = dc.init(token='test_token', local=True, timeout=10)
|
|
135
|
+
|
|
136
|
+
# 测试一个简单的API调用
|
|
137
|
+
print("✅ 测试 HSGT 基金查询接口...")
|
|
138
|
+
try:
|
|
139
|
+
result = client.hsgt_fund_page_list(page=1, page_size=5)
|
|
140
|
+
|
|
141
|
+
# 验证返回结果
|
|
142
|
+
assert isinstance(result, pd.DataFrame), "返回结果应该是DataFrame"
|
|
143
|
+
print(f" ✓ API调用成功,返回DataFrame类型")
|
|
144
|
+
print(f" ✓ 返回数据行数: {len(result)}")
|
|
145
|
+
|
|
146
|
+
# 如果有数据,验证数据结构
|
|
147
|
+
if len(result) > 0:
|
|
148
|
+
print(f" ✓ 数据列: {list(result.columns)}")
|
|
149
|
+
print(f" ✓ 示例数据:\n{result.head(2).to_string()}")
|
|
150
|
+
else:
|
|
151
|
+
print(" ⚠️ 返回空数据,这可能是因为数据库中没有数据")
|
|
152
|
+
|
|
153
|
+
except DatacenterAPIError as e:
|
|
154
|
+
if e.code == 401:
|
|
155
|
+
print(f" ✓ API认证正常工作 (401未授权,测试token无效): {e.message}")
|
|
156
|
+
else:
|
|
157
|
+
print(f" ⚠️ API返回其他错误: {e}")
|
|
158
|
+
except Exception as e:
|
|
159
|
+
# 检查是否是401认证错误(在异常消息中)
|
|
160
|
+
if "401" in str(e) or "HTTP请求失败,状态码: 401" in str(e):
|
|
161
|
+
print(f" ✓ API认证正常工作 (401未授权,测试token无效)")
|
|
162
|
+
else:
|
|
163
|
+
print(f" ❌ API调用失败: {e}")
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
print("\n🎉 真实API调用测试完成!")
|
|
167
|
+
return True
|
|
168
|
+
|
|
169
|
+
except Exception as e:
|
|
170
|
+
print(f"\n❌ 真实API调用测试失败: {e}")
|
|
171
|
+
import traceback
|
|
172
|
+
traceback.print_exc()
|
|
173
|
+
return False
|
|
174
|
+
|
|
175
|
+
|
|
176
|
+
def run_unit_tests():
|
|
177
|
+
"""运行单元测试"""
|
|
178
|
+
print("\n🚀 开始运行 DataCenter Client 单元测试...")
|
|
179
|
+
|
|
180
|
+
tests = [
|
|
181
|
+
test_client_initialization,
|
|
182
|
+
test_dynamic_method_creation,
|
|
183
|
+
test_real_api_call
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
passed = 0
|
|
187
|
+
total = len(tests)
|
|
188
|
+
|
|
189
|
+
for test in tests:
|
|
190
|
+
if test():
|
|
191
|
+
passed += 1
|
|
192
|
+
|
|
193
|
+
print("\n" + "=" * 50)
|
|
194
|
+
print("📊 测试结果汇总")
|
|
195
|
+
print("=" * 50)
|
|
196
|
+
print(f"✅ 通过: {passed}/{total}")
|
|
197
|
+
print(f"❌ 失败: {total - passed}/{total}")
|
|
198
|
+
|
|
199
|
+
if passed == total:
|
|
200
|
+
print("\n🎉 所有测试通过! DataCenter Client 工作正常!")
|
|
201
|
+
else:
|
|
202
|
+
print(f"\n⚠️ 有 {total - passed} 个测试失败,请检查问题")
|
|
203
|
+
|
|
204
|
+
return passed == total
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def print_usage_info():
|
|
208
|
+
"""打印使用说明"""
|
|
209
|
+
print("\n" + "=" * 70)
|
|
210
|
+
print("🎉 DataCenter Client 已完全迁移到 Handler 架构!")
|
|
211
|
+
print("=" * 70)
|
|
212
|
+
print("")
|
|
213
|
+
print("📦 架构变更:")
|
|
214
|
+
print(" ✅ 所有模块特定的 client 文件已被移除")
|
|
215
|
+
print(" ✅ 所有对应的 DTO 文件已被移除")
|
|
216
|
+
print(" ✅ 所有模块特定的测试文件已被移除")
|
|
217
|
+
print(" ✅ 现在使用统一的 Universal Client 模式")
|
|
218
|
+
print("")
|
|
219
|
+
print("🚀 推荐的使用方式:")
|
|
220
|
+
print(" 1. 使用 Universal Client (datacenter_client.init_client)")
|
|
221
|
+
print(" 2. 通过统一的 Handler 接口访问所有功能")
|
|
222
|
+
print(" 3. 直接通过 HTTP 调用测试 API 接口")
|
|
223
|
+
print("")
|
|
224
|
+
print("💡 使用示例:")
|
|
225
|
+
print(" import dc_client as dc")
|
|
226
|
+
print(" # 本地开发")
|
|
227
|
+
print(" client = dc.init_client(token='your_token', local=True)")
|
|
228
|
+
print(" # 或指定显式URL")
|
|
229
|
+
print(" client = dc.init_client(token='your_token', base_url='http://localhost:10000')")
|
|
230
|
+
print("")
|
|
231
|
+
print(" # Margin Account")
|
|
232
|
+
print(" result = client.margin_account_page_list(page=1, page_size=10)")
|
|
233
|
+
print(" result = client.margin_account_list(limit=20)")
|
|
234
|
+
print("")
|
|
235
|
+
print(" # Margin Analysis")
|
|
236
|
+
print(" result = client.margin_analysis_page_list(page=1, page_size=10)")
|
|
237
|
+
print(" result = client.margin_analysis_list(limit=20)")
|
|
238
|
+
print("")
|
|
239
|
+
print(" # Margin Detail")
|
|
240
|
+
print(" result = client.margin_detail_page_list_by_date(page=1, page_size=10)")
|
|
241
|
+
print(" result = client.margin_detail_page_list_by_stock(page=1, page_size=10)")
|
|
242
|
+
print(" result = client.margin_detail_list_by_stock(stock_code='000001.SZ', limit=20)")
|
|
243
|
+
print("")
|
|
244
|
+
print(" # AStock")
|
|
245
|
+
print(" result = client.a_stock_page_list(page=1, page_size=10)")
|
|
246
|
+
print(" result = client.a_stock_list(limit=20)")
|
|
247
|
+
print("")
|
|
248
|
+
print(" # HKStock")
|
|
249
|
+
print(" result = client.hk_stock_page_list(page=1, page_size=10)")
|
|
250
|
+
print(" result = client.hk_stock_list(limit=20)")
|
|
251
|
+
print("")
|
|
252
|
+
print(" # Industry")
|
|
253
|
+
print(" result = client.hs_industry_page_list(page=1, page_size=10)")
|
|
254
|
+
print(" result = client.hs_industry_list(limit=20)")
|
|
255
|
+
print(" result = client.hs_industry_company_page_list(page=1, page_size=10)")
|
|
256
|
+
print(" result = client.hs_industry_company_list(limit=20)")
|
|
257
|
+
print(" result = client.sw_industry_page_list(page=1, page_size=10)")
|
|
258
|
+
print(" result = client.sw_industry_list(limit=20)")
|
|
259
|
+
print(" result = client.sw_industry_company_page_list(page=1, page_size=10, level_type='level1')")
|
|
260
|
+
print(" result = client.sw_industry_company_list(limit=20, level_type='level1')")
|
|
261
|
+
print("")
|
|
262
|
+
print("🔍 查看所有可用的 Handler 接口:")
|
|
263
|
+
print(" curl 'http://localhost:10000/api/v1/docs/json' | python3 -c \"import sys, json; data=json.load(sys.stdin); [print(f'- {api[\"name\"]}: {api[\"description\"]}') for api in data['apis']]\"")
|
|
264
|
+
print("")
|
|
265
|
+
print("🔍 搜索特定的接口:")
|
|
266
|
+
print(" curl 'http://localhost:10000/api/v1/docs/search?q=margin'")
|
|
267
|
+
print("")
|
|
268
|
+
print("📖 Handler 模式文档:")
|
|
269
|
+
print(" - 所有接口都通过 /api/v1/dataapi/{handler_name} 访问")
|
|
270
|
+
print(" - 支持 POST 请求,JSON 格式参数")
|
|
271
|
+
print(" - 统一的响应格式和错误处理")
|
|
272
|
+
print("")
|
|
273
|
+
print("=" * 70)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def main():
|
|
277
|
+
"""主函数"""
|
|
278
|
+
if len(sys.argv) > 1:
|
|
279
|
+
arg = sys.argv[1]
|
|
280
|
+
if arg in ['-h', '--help', 'help']:
|
|
281
|
+
print_usage_info()
|
|
282
|
+
return 0
|
|
283
|
+
else:
|
|
284
|
+
print(f"Unknown argument: {arg}")
|
|
285
|
+
print("Use -h or --help for usage information.")
|
|
286
|
+
return 1
|
|
287
|
+
else:
|
|
288
|
+
# 默认运行单元测试
|
|
289
|
+
success = run_unit_tests()
|
|
290
|
+
return 0 if success else 1
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
if __name__ == "__main__":
|
|
294
|
+
sys.exit(main())
|
|
File without changes
|
dc_client/tests/base.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from typing import Dict, Any, List
|
|
3
|
+
|
|
4
|
+
from dc_client.client import DatacenterClient
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class BaseClientTest(unittest.TestCase):
|
|
8
|
+
"""基础测试类,提供通用的测试设置和方法"""
|
|
9
|
+
|
|
10
|
+
BASE_URL = "http://127.0.0.1:10000"
|
|
11
|
+
TOKEN = "4e2e929a1ba14b06b8e8ca7e7e560efe"
|
|
12
|
+
|
|
13
|
+
def setUp(self):
|
|
14
|
+
"""设置测试环境"""
|
|
15
|
+
self.client = DatacenterClient(base_url=self.BASE_URL, token=self.TOKEN)
|
|
16
|
+
|
|
17
|
+
def check_success_response(self, result: Dict[str, Any]) -> bool:
|
|
18
|
+
"""检查响应是否成功"""
|
|
19
|
+
# 检查result是否为字典且包含status和data字段
|
|
20
|
+
return isinstance(result, dict) and result.get('status') == 'success' and 'data' in result
|
|
21
|
+
|
|
22
|
+
def check_pagination_response(self, result: Any) -> bool:
|
|
23
|
+
"""检查分页响应格式"""
|
|
24
|
+
# 检查是否有status和data字段,且data中有items和pagination
|
|
25
|
+
if isinstance(result, dict) and result.get('status') == 'success' and 'data' in result:
|
|
26
|
+
data = result['data']
|
|
27
|
+
return isinstance(data, dict) and 'items' in data and 'pagination' in data
|
|
28
|
+
# 兼容旧格式
|
|
29
|
+
return isinstance(result, dict) and 'items' in result and 'pagination' in result
|
|
30
|
+
|
|
31
|
+
def print_pagination_info(self, result: Any):
|
|
32
|
+
"""打印分页信息"""
|
|
33
|
+
# 检查是否是DTO对象
|
|
34
|
+
if hasattr(result, 'status') and hasattr(result, 'data'):
|
|
35
|
+
# DTO对象
|
|
36
|
+
print(f"返回记录数: {len(result.items)}")
|
|
37
|
+
if hasattr(result, 'pagination') and result.pagination:
|
|
38
|
+
print(f"分页信息: {result.pagination}")
|
|
39
|
+
if result.items:
|
|
40
|
+
print(f"第一条记录: {result.items[0]}")
|
|
41
|
+
elif isinstance(result, dict) and result.get('status') == 'success' and 'data' in result:
|
|
42
|
+
# 新的响应格式
|
|
43
|
+
data = result['data']
|
|
44
|
+
if isinstance(data, dict) and 'items' in data:
|
|
45
|
+
print(f"返回记录数: {len(data['items'])}")
|
|
46
|
+
if 'pagination' in data:
|
|
47
|
+
print(f"分页信息: {data['pagination']}")
|
|
48
|
+
if data['items']:
|
|
49
|
+
print(f"第一条记录: {data['items'][0]}")
|
|
50
|
+
else:
|
|
51
|
+
print("返回数据格式不符合预期")
|
|
52
|
+
elif isinstance(result, dict) and 'items' in result and 'pagination' in result:
|
|
53
|
+
# 旧格式
|
|
54
|
+
print(f"返回记录数: {len(result['items'])}")
|
|
55
|
+
print(f"分页信息: {result['pagination']}")
|
|
56
|
+
if result['items']:
|
|
57
|
+
print(f"第一条记录: {result['items'][0]}")
|
|
58
|
+
else:
|
|
59
|
+
print("返回数据格式不符合预期")
|
|
60
|
+
|
|
61
|
+
def print_list_info(self, result: Any):
|
|
62
|
+
"""打印列表信息"""
|
|
63
|
+
# 检查是否是DTO对象
|
|
64
|
+
if hasattr(result, 'status') and hasattr(result, 'data'):
|
|
65
|
+
# DTO对象
|
|
66
|
+
print(f"返回记录数: {len(result.items)}")
|
|
67
|
+
if hasattr(result, 'total'):
|
|
68
|
+
print(f"总记录数: {result.total}")
|
|
69
|
+
if result.items:
|
|
70
|
+
print(f"第一条记录: {result.items[0]}")
|
|
71
|
+
elif isinstance(result, dict) and result.get('status') == 'success' and 'data' in result:
|
|
72
|
+
# 新的响应格式
|
|
73
|
+
data = result['data']
|
|
74
|
+
if isinstance(data, dict) and 'items' in data:
|
|
75
|
+
print(f"返回记录数: {len(data['items'])}")
|
|
76
|
+
if 'total' in data:
|
|
77
|
+
print(f"总记录数: {data['total']}")
|
|
78
|
+
if data['items']:
|
|
79
|
+
print(f"第一条记录: {data['items'][0]}")
|
|
80
|
+
elif isinstance(data, list):
|
|
81
|
+
print(f"返回记录数: {len(data)}")
|
|
82
|
+
if data:
|
|
83
|
+
print(f"第一条记录: {data[0]}")
|
|
84
|
+
else:
|
|
85
|
+
print("返回数据格式不符合预期")
|
|
86
|
+
elif isinstance(result, list):
|
|
87
|
+
# 纯列表格式
|
|
88
|
+
print(f"返回记录数: {len(result)}")
|
|
89
|
+
if result:
|
|
90
|
+
print(f"第一条记录: {result[0]}")
|
|
91
|
+
elif isinstance(result, dict):
|
|
92
|
+
# 如果是字典,可能是分页数据
|
|
93
|
+
if 'items' in result:
|
|
94
|
+
print(f"返回记录数: {len(result['items'])}")
|
|
95
|
+
if result['items']:
|
|
96
|
+
print(f"第一条记录: {result['items'][0]}")
|
|
97
|
+
else:
|
|
98
|
+
print("返回数据格式不符合预期")
|
|
99
|
+
else:
|
|
100
|
+
print("返回数据格式不符合预期")
|
|
101
|
+
|
|
102
|
+
def print_item_info(self, result: Any):
|
|
103
|
+
"""打印单项信息"""
|
|
104
|
+
# 检查是否是DTO对象
|
|
105
|
+
if hasattr(result, 'status') and hasattr(result, 'data'):
|
|
106
|
+
# DTO对象
|
|
107
|
+
# 根据不同的DTO类型,访问不同的属性
|
|
108
|
+
if hasattr(result, 'industries'):
|
|
109
|
+
print(f"返回数据: {result.industries}")
|
|
110
|
+
elif hasattr(result, 'industry'):
|
|
111
|
+
print(f"返回数据: {result.industry}")
|
|
112
|
+
elif hasattr(result, 'summary'):
|
|
113
|
+
print(f"返回数据: {result.summary}")
|
|
114
|
+
else:
|
|
115
|
+
print(f"返回数据: {result.data}")
|
|
116
|
+
elif isinstance(result, dict) and result.get('status') == 'success' and 'data' in result:
|
|
117
|
+
# 新的响应格式
|
|
118
|
+
print(f"返回数据: {result['data']}")
|
|
119
|
+
elif isinstance(result, dict):
|
|
120
|
+
# 旧格式或其他格式
|
|
121
|
+
print(f"返回数据: {result}")
|
|
122
|
+
else:
|
|
123
|
+
print("请求失败")
|