├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── pyproject.toml
└── vnpy_rest
├── __init__.py
└── rest_client.py
/.gitignore:
--------------------------------------------------------------------------------
1 | # Byte-compiled / optimized / DLL files
2 | __pycache__/
3 | *.py[cod]
4 | *$py.class
5 |
6 | # C extensions
7 | *.so
8 |
9 | # Distribution / packaging
10 | .Python
11 | build/
12 | develop-eggs/
13 | dist/
14 | downloads/
15 | eggs/
16 | .eggs/
17 | lib/
18 | lib64/
19 | parts/
20 | sdist/
21 | var/
22 | wheels/
23 | pip-wheel-metadata/
24 | share/python-wheels/
25 | *.egg-info/
26 | .installed.cfg
27 | *.egg
28 | MANIFEST
29 |
30 | # PyInstaller
31 | # Usually these files are written by a python script from a template
32 | # before PyInstaller builds the exe, so as to inject date/other infos into it.
33 | *.manifest
34 | *.spec
35 |
36 | # Installer logs
37 | pip-log.txt
38 | pip-delete-this-directory.txt
39 |
40 | # Unit test / coverage reports
41 | htmlcov/
42 | .tox/
43 | .nox/
44 | .coverage
45 | .coverage.*
46 | .cache
47 | nosetests.xml
48 | coverage.xml
49 | *.cover
50 | *.py,cover
51 | .hypothesis/
52 | .pytest_cache/
53 |
54 | # Translations
55 | *.mo
56 | *.pot
57 |
58 | # Django stuff:
59 | *.log
60 | local_settings.py
61 | db.sqlite3
62 | db.sqlite3-journal
63 |
64 | # Flask stuff:
65 | instance/
66 | .webassets-cache
67 |
68 | # Scrapy stuff:
69 | .scrapy
70 |
71 | # Sphinx documentation
72 | docs/_build/
73 |
74 | # PyBuilder
75 | target/
76 |
77 | # Jupyter Notebook
78 | .ipynb_checkpoints
79 |
80 | # IPython
81 | profile_default/
82 | ipython_config.py
83 |
84 | # pyenv
85 | .python-version
86 |
87 | # pipenv
88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies
90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not
91 | # install all needed dependencies.
92 | #Pipfile.lock
93 |
94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95 | __pypackages__/
96 |
97 | # Celery stuff
98 | celerybeat-schedule
99 | celerybeat.pid
100 |
101 | # SageMath parsed files
102 | *.sage.py
103 |
104 | # Environments
105 | .env
106 | .venv
107 | env/
108 | venv/
109 | ENV/
110 | env.bak/
111 | venv.bak/
112 |
113 | # Spyder project settings
114 | .spyderproject
115 | .spyproject
116 |
117 | # Rope project settings
118 | .ropeproject
119 |
120 | # mkdocs documentation
121 | /site
122 |
123 | # mypy
124 | .mypy_cache/
125 | .dmypy.json
126 | dmypy.json
127 |
128 | # Pyre type checker
129 | .pyre/
130 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # 1.2.0版本
2 |
3 | 1. 替换aiohttp异步通讯模式,改为使用requests多线程模式
4 | 2. 替换使用pyproject.toml配置
5 | 3. ruff和mypy代码质量优化
6 |
7 | # 1.1.1版本
8 |
9 | 1. 调整connector的初始化位置
10 |
11 | # 1.1.0版本
12 |
13 | 1. 关闭底层连接的SSL检查,解决Mac系统报错
14 |
15 | # 1.0.9版本
16 |
17 | 1. 对于同步request函数也支持json参数
18 |
19 | # 1.0.8版本
20 |
21 | 1. 发送请求时,支持传入json参数
22 |
23 | # 1.0.7版本
24 |
25 | 1. 在Windows系统上必须使用Selector事件循环
26 | 2. REST客户端停止时,确保关闭所有会话
27 | 3. 等待异步关闭任务完成后,才停止事件循环
28 |
29 | # 1.0.6版本
30 |
31 | 1. 修复aiohttp的代理参数proxy传空时必须为None的问题
32 |
33 | # 1.0.5版本
34 |
35 | 1. 对Python 3.10后asyncio的修改支持
36 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2015-present, Xiaoyou Chen
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # VeighNa框架的REST API客户端
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | ## 说明
15 |
16 | 基于requests开发的多线程REST API客户端,用于开发高性能的REST交易接口。
17 |
18 | ## 安装
19 |
20 | 安装环境推荐基于4.0.0版本以上的【[**VeighNa Studio**](https://www.vnpy.com)】。
21 |
22 | 直接使用pip命令:
23 |
24 | ```
25 | pip install vnpy_rest
26 | ```
27 |
28 | 下载解压后在cmd中运行
29 |
30 | ```
31 | pip install .
32 | ```
33 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "vnpy_rest"
3 | dynamic = ["version"]
4 | description = "REST API client for vn.py quant trading framework."
5 | readme = "README.md"
6 | license = {text = "MIT"}
7 | authors = [{name = "Xiaoyou Chen", email = "xiaoyou.chen@mail.vnpy.com"}]
8 | classifiers = [
9 | "Development Status :: 5 - Production/Stable",
10 | "License :: OSI Approved :: MIT License",
11 | "Operating System :: OS Independent",
12 | "Programming Language :: Python :: 3",
13 | "Programming Language :: Python :: 3.10",
14 | "Programming Language :: Python :: 3.11",
15 | "Programming Language :: Python :: 3.12",
16 | "Programming Language :: Python :: 3.13",
17 | "Programming Language :: Python :: Implementation :: CPython",
18 | "Topic :: Office/Business :: Financial :: Investment",
19 | "Natural Language :: Chinese (Simplified)",
20 | "Typing :: Typed",
21 | ]
22 | requires-python = ">=3.10"
23 | dependencies = [
24 | "requests>=2.32.3",
25 | ]
26 |
27 | keywords = ["quant", "quantitative", "investment", "trading", "algotrading"]
28 |
29 | [project.urls]
30 | "Homepage" = "https://www.vnpy.com"
31 | "Documentation" = "https://www.vnpy.com/docs"
32 | "Changes" = "https://github.com/vnpy/vnpy_rest/blob/master/CHANGELOG.md"
33 | "Source" = "https://github.com/vnpy/vnpy_rest"
34 | "Forum" = "https://www.vnpy.com/forum"
35 |
36 | [build-system]
37 | requires = ["hatchling>=1.27.0"]
38 | build-backend = "hatchling.build"
39 |
40 | [tool.hatch.version]
41 | path = "vnpy_rest/__init__.py"
42 | pattern = "__version__ = ['\"](?P[^'\"]+)['\"]"
43 |
44 | [tool.hatch.build.targets.wheel]
45 | packages = ["vnpy_rest"]
46 | include-package-data = true
47 |
48 | [tool.hatch.build.targets.sdist]
49 | include = ["vnpy_rest*"]
50 |
51 | [tool.ruff]
52 | target-version = "py310"
53 | output-format = "full"
54 |
55 | [tool.ruff.lint]
56 | select = [
57 | "B", # flake8-bugbear
58 | "E", # pycodestyle error
59 | "F", # pyflakes
60 | "UP", # pyupgrade
61 | "W", # pycodestyle warning
62 | ]
63 | ignore = ["E501"]
64 |
65 | [tool.mypy]
66 | python_version = "3.10"
67 | warn_return_any = true
68 | warn_unused_configs = true
69 | disallow_untyped_defs = true
70 | disallow_incomplete_defs = true
71 | check_untyped_defs = true
72 | disallow_untyped_decorators = true
73 | no_implicit_optional = true
74 | strict_optional = true
75 | warn_redundant_casts = true
76 | warn_unused_ignores = true
77 | warn_no_return = true
--------------------------------------------------------------------------------
/vnpy_rest/__init__.py:
--------------------------------------------------------------------------------
1 | # The MIT License (MIT)
2 | #
3 | # Copyright (c) 2015-present, Xiaoyou Chen
4 | #
5 | # Permission is hereby granted, free of charge, to any person obtaining a copy
6 | # of this software and associated documentation files (the "Software"), to deal
7 | # in the Software without restriction, including without limitation the rights
8 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | # copies of the Software, and to permit persons to whom the Software is
10 | # furnished to do so, subject to the following conditions:
11 | #
12 | # The above copyright notice and this permission notice shall be included in all
13 | # copies or substantial portions of the Software.
14 | #
15 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | # SOFTWARE.
22 |
23 | from .rest_client import RestClient, Request, Response
24 |
25 |
26 | __all__ = [
27 | "RestClient",
28 | "Request",
29 | "Response"
30 | ]
31 |
32 |
33 | __version__ = "1.2.0"
34 |
--------------------------------------------------------------------------------
/vnpy_rest/rest_client.py:
--------------------------------------------------------------------------------
1 | import sys
2 | import traceback
3 | from datetime import datetime
4 | from multiprocessing.dummy import Pool
5 | from multiprocessing.pool import ThreadPool
6 | from queue import Empty, Queue
7 | from typing import Any
8 | from collections.abc import Callable
9 | from types import TracebackType
10 |
11 | import requests
12 |
13 |
14 | CALLBACK_TYPE = Callable[[dict | None, "Request"], Any]
15 | ON_FAILED_TYPE = Callable[[int, "Request"], Any]
16 | ON_ERROR_TYPE = Callable[[type[BaseException], BaseException, TracebackType, "Request"], Any]
17 |
18 |
19 | Response = requests.Response
20 |
21 |
22 | class Request:
23 | """
24 | Request object
25 |
26 | method: API request method (GET, POST, PUT, DELETE, QUERY)
27 | path: API request path (without base URL)
28 | callback: Callback function on request success
29 | params: Dictionary of request parameters
30 | data: Request body data, dictionaries will be automatically converted to JSON
31 | headers: Dictionary of request headers
32 | on_failed: Callback function on request failure
33 | on_error: Callback function on request exception
34 | extra: Any additional data (for use in callbacks)
35 | """
36 |
37 | def __init__(
38 | self,
39 | method: str,
40 | path: str,
41 | params: dict | None,
42 | data: dict | str | None,
43 | headers: dict | None,
44 | callback: CALLBACK_TYPE | None = None,
45 | on_failed: ON_FAILED_TYPE | None = None,
46 | on_error: ON_ERROR_TYPE | None = None,
47 | extra: Any | None = None,
48 | ) -> None:
49 | """Initialize a request object"""
50 | self.method: str = method
51 | self.path: str = path
52 | self.callback: CALLBACK_TYPE | None = callback
53 | self.params: dict | None = params
54 | self.data: dict | str | None = data
55 | self.headers: dict | None = headers
56 |
57 | self.on_failed: ON_FAILED_TYPE | None = on_failed
58 | self.on_error: ON_ERROR_TYPE | None = on_error
59 | self.extra: Any | None = extra
60 |
61 | self.response: requests.Response | None = None
62 |
63 | def __str__(self) -> str:
64 | """String representation of the request"""
65 | if self.response is None:
66 | status_code = "terminated"
67 | else:
68 | status_code = str(self.response.status_code)
69 |
70 | text: str = f"request : {self.method} {self.path} because {status_code}: \n"
71 | text += f"headers: {self.headers}\n"
72 | text += f"params: {self.params}\n"
73 | text += f"data: {self.data!r}\n"
74 | text += f"response: {self.response.text if self.response else ''}\n"
75 | return text
76 |
77 |
78 | class RestClient:
79 | """
80 | Asynchronous client for various REST APIs
81 |
82 | * Override the sign method to implement request signature logic
83 | * Override the on_failed method to implement standard callback handling for request failures
84 | * Override the on_error method to implement standard callback handling for request exceptions
85 | """
86 |
87 | def __init__(self) -> None:
88 | """Constructor"""
89 | self.url_base: str = ""
90 | self.active: bool = False
91 |
92 | self.queue: Queue = Queue()
93 |
94 | self.proxies: dict | None = None
95 |
96 | def init(
97 | self,
98 | url_base: str,
99 | proxy_host: str = "",
100 | proxy_port: int = 0
101 | ) -> None:
102 | """
103 | Initialize the client with the REST API base URL
104 |
105 | :param url_base: Base URL for the REST API
106 | :param proxy_host: Proxy host address
107 | :param proxy_port: Proxy port number
108 | """
109 | self.url_base = url_base
110 |
111 | if proxy_host and proxy_port:
112 | proxy: str = f"http://{proxy_host}:{proxy_port}"
113 | self.proxies = {"http": proxy, "https": proxy}
114 |
115 | def start(self, n: int = 5) -> None:
116 | """
117 | Start the client
118 |
119 | :param n: Number of worker threads
120 | """
121 | if self.active:
122 | return
123 | self.active = True
124 |
125 | self.pool: ThreadPool = Pool(n)
126 | self.pool.apply_async(self.run)
127 |
128 | def stop(self) -> None:
129 | """Stop the client"""
130 | self.active = False
131 |
132 | def join(self) -> None:
133 | """Wait for threads to complete"""
134 | self.queue.join()
135 |
136 | def add_request(
137 | self,
138 | method: str,
139 | path: str,
140 | callback: CALLBACK_TYPE,
141 | params: dict | None = None,
142 | data: dict | str | None = None,
143 | headers: dict | None = None,
144 | on_failed: ON_FAILED_TYPE | None = None,
145 | on_error: ON_ERROR_TYPE | None = None,
146 | extra: Any | None = None,
147 | ) -> Request:
148 | """
149 | Add a new request task
150 |
151 | :param method: HTTP method
152 | :param path: API endpoint path
153 | :param callback: Callback function for successful responses
154 | :param params: Query parameters
155 | :param data: Request body data
156 | :param headers: HTTP headers
157 | :param on_failed: Callback for failed requests
158 | :param on_error: Callback for request exceptions
159 | :param extra: Additional data to pass to callbacks
160 | :return: Request object
161 | """
162 | request: Request = Request(
163 | method,
164 | path,
165 | params,
166 | data,
167 | headers,
168 | callback,
169 | on_failed,
170 | on_error,
171 | extra,
172 | )
173 | self.queue.put(request)
174 | return request
175 |
176 | def run(self) -> None:
177 | """Process tasks in each thread"""
178 | try:
179 | session = requests.session()
180 | while self.active:
181 | try:
182 | request = self.queue.get(timeout=1)
183 | try:
184 | self.process_request(request, session)
185 | finally:
186 | self.queue.task_done()
187 | except Empty:
188 | pass
189 | except Exception:
190 | exc, value, tb = sys.exc_info()
191 | if exc and value and tb:
192 | self.on_error(exc, value, tb, None)
193 |
194 | def sign(self, request: Request) -> Request:
195 | """
196 | Signature function (override to implement specific signature logic)
197 |
198 | :param request: Request to sign
199 | :return: Signed request
200 | """
201 | return request
202 |
203 | def on_failed(self, status_code: int, request: Request) -> None:
204 | """
205 | Default callback for request failures
206 |
207 | :param status_code: HTTP status code
208 | :param request: Failed request
209 | """
210 | print("RestClient on failed" + "-" * 10)
211 | print(str(request))
212 |
213 | def on_error(
214 | self,
215 | exc: type[BaseException],
216 | value: BaseException,
217 | tb: TracebackType,
218 | request: Request | None,
219 | ) -> None:
220 | """
221 | Default callback for request exceptions
222 |
223 | :param exc: Exception class
224 | :param value: Exception instance
225 | :param tb: Traceback object
226 | :param request: Request that caused the exception
227 | """
228 | try:
229 | print("RestClient on error" + "-" * 10)
230 | print(self.exception_detail(exc, value, tb, request))
231 | except Exception:
232 | traceback.print_exc()
233 |
234 | def exception_detail(
235 | self,
236 | exc: type[BaseException],
237 | value: BaseException,
238 | tb: TracebackType,
239 | request: Request | None,
240 | ) -> str:
241 | """
242 | Convert exception information to string
243 |
244 | :param exc: Exception class
245 | :param value: Exception instance
246 | :param tb: Traceback object
247 | :param request: Request that caused the exception
248 | :return: Formatted exception details
249 | """
250 | text = f"[{datetime.now().isoformat()}]: Unhandled RestClient Error:{exc}\n"
251 | text += f"request:{request}\n"
252 | text += "Exception trace: \n"
253 | text += "".join(traceback.format_exception(exc, value, tb))
254 | return text
255 |
256 | def process_request(self, request: Request, session: requests.Session) -> None:
257 | """
258 | Send request to server and process response
259 |
260 | :param request: Request to process
261 | :param session: Requests session
262 | """
263 | try:
264 | # Sign the request
265 | request = self.sign(request)
266 |
267 | # Send synchronous request
268 | response: Response = session.request(
269 | request.method,
270 | self.make_full_url(request.path),
271 | headers=request.headers,
272 | params=request.params,
273 | data=request.data,
274 | proxies=self.proxies,
275 | )
276 |
277 | # Bind response to request
278 | request.response = response
279 |
280 | # Parse response data
281 | status_code = response.status_code
282 |
283 | if status_code // 100 == 2: # 2xx indicates success
284 | json_body: dict | None = None
285 |
286 | if status_code != 204:
287 | json_body = response.json()
288 |
289 | if request.callback:
290 | request.callback(json_body, request)
291 | else:
292 | if request.on_failed:
293 | request.on_failed(status_code, request)
294 | else:
295 | self.on_failed(status_code, request)
296 | except Exception:
297 | # Get exception information
298 | exc, value, tb = sys.exc_info()
299 |
300 | # Push exception callback
301 | if exc and value and tb:
302 | if request.on_error:
303 | request.on_error(exc, value, tb, request)
304 | else:
305 | self.on_error(exc, value, tb, request)
306 |
307 | def make_full_url(self, path: str) -> str:
308 | """
309 | Combine base URL and path to generate full request URL
310 |
311 | :param path: API endpoint path
312 | :return: Complete URL
313 | """
314 | return self.url_base + path
315 |
316 | def request(
317 | self,
318 | method: str,
319 | path: str,
320 | params: dict | None = None,
321 | data: dict | None = None,
322 | headers: dict | None = None,
323 | ) -> Response:
324 | """
325 | Make a synchronous request
326 |
327 | :param method: HTTP method
328 | :param path: API endpoint path
329 | :param params: Query parameters
330 | :param data: Request body data
331 | :param headers: HTTP headers
332 | :return: Response object
333 | """
334 | # Create request object
335 | request: Request = Request(
336 | method,
337 | path,
338 | params,
339 | data,
340 | headers
341 | )
342 |
343 | # Sign the request
344 | request = self.sign(request)
345 |
346 | # Send synchronous request
347 | response: Response = requests.request(
348 | request.method,
349 | self.make_full_url(request.path),
350 | headers=request.headers,
351 | params=request.params,
352 | data=request.data,
353 | proxies=self.proxies,
354 | )
355 | return response
356 |
--------------------------------------------------------------------------------