kaq-quant-common 0.2.16__tar.gz → 0.2.18__tar.gz
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.
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/PKG-INFO +1 -1
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/api/common/__init__.py +1 -1
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/api/common/api_interface.py +38 -38
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/api/rest/api_client_base.py +187 -187
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/api/rest/instruction/helper/commission_helper.py +141 -141
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/api/rest/instruction/helper/mock_order_helper.py +346 -346
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/api/rest/instruction/helper/order_helper.py +362 -362
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/api/rest/instruction/instruction_client.py +40 -2
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/api/rest/instruction/instruction_server_base.py +62 -53
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/api/rest/instruction/models/__init__.py +17 -17
- kaq_quant_common-0.2.18/kaq_quant_common/api/rest/instruction/models/kline.py +60 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/api/rest/instruction/models/transfer.py +32 -32
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/api/ws/exchange/models.py +23 -23
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/api/ws/exchange/ws_exchange_server.py +440 -440
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/common/ddb_table_monitor.py +106 -106
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/common/http_monitor.py +69 -69
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/common/modules/limit_order_helper.py +12 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/common/monitor_base.py +84 -84
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/common/monitor_group.py +97 -97
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/common/ws_wrapper.py +21 -21
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/utils/logger_utils.py +4 -4
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/utils/signal_utils.py +23 -23
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/utils/uuid_utils.py +5 -5
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/pyproject.toml +1 -1
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/README.md +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/__init__.py +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/api/__init__.py +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/api/common/auth.py +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/api/rest/__init__.py +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/api/rest/api_server_base.py +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/api/rest/instruction/models/account.py +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/api/rest/instruction/models/loan.py +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/api/rest/instruction/models/order.py +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/api/rest/instruction/models/position.py +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/api/ws/__init__.py +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/api/ws/exchange/ws_exchange_client.py +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/api/ws/instruction/__init__.py +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/api/ws/instruction/ws_instruction_client.py +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/api/ws/instruction/ws_instruction_server_base.py +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/api/ws/models.py +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/api/ws/ws_client_base.py +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/api/ws/ws_server_base.py +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/common/__init__.py +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/common/modules/funding_rate_helper.py +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/common/modules/limit_order_symbol_monitor.py +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/common/modules/limit_order_symbol_monitor_group.py +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/common/redis_table_monitor.py +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/common/statistics/funding_rate_history_statistics.py +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/common/statistics/kline_history_statistics.py +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/config/config.yaml +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/resources/__init__.py +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/resources/kaq_ddb_pool_stream_read_resources.py +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/resources/kaq_ddb_stream_init_resources.py +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/resources/kaq_ddb_stream_read_resources.py +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/resources/kaq_ddb_stream_write_resources.py +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/resources/kaq_mysql_init_resources.py +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/resources/kaq_mysql_resources.py +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/resources/kaq_postgresql_resources.py +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/resources/kaq_quant_hive_resources.py +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/resources/kaq_redis_resources.py +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/utils/__init__.py +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/utils/dagster_job_check_utils.py +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/utils/dagster_utils.py +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/utils/date_util.py +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/utils/enums_utils.py +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/utils/error_utils.py +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/utils/hash_utils.py +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/utils/log_time_utils.py +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/utils/mytt_utils.py +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/utils/sqlite_utils.py +0 -0
- {kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/utils/yml_utils.py +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
|
|
1
|
+
|
{kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/api/common/api_interface.py
RENAMED
|
@@ -1,38 +1,38 @@
|
|
|
1
|
-
from abc import ABC
|
|
2
|
-
from functools import wraps
|
|
3
|
-
from typing import Callable, Type
|
|
4
|
-
|
|
5
|
-
from pydantic import BaseModel
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
def api_method(request_model: Type[BaseModel], response_model: Type[BaseModel]):
|
|
9
|
-
"""
|
|
10
|
-
api 方法注解
|
|
11
|
-
:param request_model: 请求模型
|
|
12
|
-
:param response_model: 响应模型
|
|
13
|
-
:return:
|
|
14
|
-
"""
|
|
15
|
-
|
|
16
|
-
def decorator(func: Callable):
|
|
17
|
-
# 将注解信息绑定到原始函数
|
|
18
|
-
func._is_api_method = True
|
|
19
|
-
func._request_model = request_model
|
|
20
|
-
func._response_model = response_model
|
|
21
|
-
|
|
22
|
-
@wraps(func)
|
|
23
|
-
def wrapper(*args, **kwargs):
|
|
24
|
-
return func(*args, **kwargs)
|
|
25
|
-
|
|
26
|
-
# 同步注解信息到包装函数,便于通过inspect发现
|
|
27
|
-
wrapper._is_api_method = True
|
|
28
|
-
wrapper._request_model = request_model
|
|
29
|
-
wrapper._response_model = response_model
|
|
30
|
-
|
|
31
|
-
return wrapper
|
|
32
|
-
|
|
33
|
-
return decorator
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
# 定义 api 接口,暂时没啥用
|
|
37
|
-
class ApiInterface(ABC):
|
|
38
|
-
pass
|
|
1
|
+
from abc import ABC
|
|
2
|
+
from functools import wraps
|
|
3
|
+
from typing import Callable, Type
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def api_method(request_model: Type[BaseModel], response_model: Type[BaseModel]):
|
|
9
|
+
"""
|
|
10
|
+
api 方法注解
|
|
11
|
+
:param request_model: 请求模型
|
|
12
|
+
:param response_model: 响应模型
|
|
13
|
+
:return:
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
def decorator(func: Callable):
|
|
17
|
+
# 将注解信息绑定到原始函数
|
|
18
|
+
func._is_api_method = True
|
|
19
|
+
func._request_model = request_model
|
|
20
|
+
func._response_model = response_model
|
|
21
|
+
|
|
22
|
+
@wraps(func)
|
|
23
|
+
def wrapper(*args, **kwargs):
|
|
24
|
+
return func(*args, **kwargs)
|
|
25
|
+
|
|
26
|
+
# 同步注解信息到包装函数,便于通过inspect发现
|
|
27
|
+
wrapper._is_api_method = True
|
|
28
|
+
wrapper._request_model = request_model
|
|
29
|
+
wrapper._response_model = response_model
|
|
30
|
+
|
|
31
|
+
return wrapper
|
|
32
|
+
|
|
33
|
+
return decorator
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
# 定义 api 接口,暂时没啥用
|
|
37
|
+
class ApiInterface(ABC):
|
|
38
|
+
pass
|
{kaq_quant_common-0.2.16 → kaq_quant_common-0.2.18}/kaq_quant_common/api/rest/api_client_base.py
RENAMED
|
@@ -1,187 +1,187 @@
|
|
|
1
|
-
from typing import Optional, Type, TypeVar, Callable
|
|
2
|
-
import asyncio
|
|
3
|
-
import threading
|
|
4
|
-
import time
|
|
5
|
-
|
|
6
|
-
import requests
|
|
7
|
-
try:
|
|
8
|
-
import httpx
|
|
9
|
-
HTTPX_AVAILABLE = True
|
|
10
|
-
except ImportError:
|
|
11
|
-
HTTPX_AVAILABLE = False
|
|
12
|
-
|
|
13
|
-
from kaq_quant_common.api.common.auth import get_auth_token
|
|
14
|
-
from kaq_quant_common.utils import logger_utils
|
|
15
|
-
from pydantic import BaseModel
|
|
16
|
-
|
|
17
|
-
R = TypeVar("R", bound=BaseModel)
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
class ApiClientBase:
|
|
21
|
-
"""
|
|
22
|
-
api 客户端
|
|
23
|
-
"""
|
|
24
|
-
|
|
25
|
-
# 类级别的共享 event loop 线程
|
|
26
|
-
_shared_loop: Optional[asyncio.AbstractEventLoop] = None
|
|
27
|
-
_shared_loop_thread: Optional[threading.Thread] = None
|
|
28
|
-
_loop_lock = threading.Lock()
|
|
29
|
-
|
|
30
|
-
def __init__(self, base_url: str, token: Optional[str] = None):
|
|
31
|
-
self._base_url = base_url.rstrip("/")
|
|
32
|
-
self._token = token if token is not None else get_auth_token()
|
|
33
|
-
self._logger = logger_utils.get_logger(self)
|
|
34
|
-
# 异步客户端(懒加载)
|
|
35
|
-
self._async_client: Optional[httpx.AsyncClient] = None
|
|
36
|
-
|
|
37
|
-
@classmethod
|
|
38
|
-
def _ensure_shared_loop(cls):
|
|
39
|
-
"""确保共享的 event loop 已创建并运行"""
|
|
40
|
-
if cls._shared_loop is None or not cls._shared_loop.is_running():
|
|
41
|
-
with cls._loop_lock:
|
|
42
|
-
# 双重检查
|
|
43
|
-
if cls._shared_loop is None or not cls._shared_loop.is_running():
|
|
44
|
-
def run_loop():
|
|
45
|
-
"""在后台线程中运行 event loop"""
|
|
46
|
-
cls._shared_loop = asyncio.new_event_loop()
|
|
47
|
-
asyncio.set_event_loop(cls._shared_loop)
|
|
48
|
-
cls._shared_loop.run_forever()
|
|
49
|
-
|
|
50
|
-
cls._shared_loop_thread = threading.Thread(
|
|
51
|
-
target=run_loop,
|
|
52
|
-
daemon=True,
|
|
53
|
-
name="ApiClient-EventLoop"
|
|
54
|
-
)
|
|
55
|
-
cls._shared_loop_thread.start()
|
|
56
|
-
|
|
57
|
-
# 等待 loop 启动
|
|
58
|
-
while cls._shared_loop is None:
|
|
59
|
-
time.sleep(0.01)
|
|
60
|
-
|
|
61
|
-
# 发送请求
|
|
62
|
-
def _make_request(self, method_name: str, request_data: BaseModel, response_model: Type[R]) -> R:
|
|
63
|
-
url = f"{self._base_url}/api/{method_name}"
|
|
64
|
-
headers = {}
|
|
65
|
-
if self._token:
|
|
66
|
-
headers["Authorization"] = f"Bearer {self._token}"
|
|
67
|
-
try:
|
|
68
|
-
# 发送post请求
|
|
69
|
-
response = requests.post(url, json=request_data.model_dump(), headers=headers or None)
|
|
70
|
-
# 检查响应状态码,如果不成功,则尝试解析错误信息并抛出异常
|
|
71
|
-
if not response.ok:
|
|
72
|
-
try:
|
|
73
|
-
error_data = response.json()
|
|
74
|
-
error_message = error_data.get("error", response.text)
|
|
75
|
-
except ValueError:
|
|
76
|
-
error_message = response.text
|
|
77
|
-
raise requests.exceptions.HTTPError(f"HTTP error occurred: {response.status_code} - {error_message}", response=response)
|
|
78
|
-
# 返回请求结果
|
|
79
|
-
return response_model(**response.json())
|
|
80
|
-
except requests.exceptions.RequestException as e:
|
|
81
|
-
self._logger.error(f"An error occurred: {e}")
|
|
82
|
-
raise
|
|
83
|
-
|
|
84
|
-
# 异步发送请求
|
|
85
|
-
async def _make_request_async(self, method_name: str, request_data: BaseModel, response_model: Type[R]) -> R:
|
|
86
|
-
"""
|
|
87
|
-
异步发送请求
|
|
88
|
-
|
|
89
|
-
Args:
|
|
90
|
-
method_name: API方法名
|
|
91
|
-
request_data: 请求数据
|
|
92
|
-
response_model: 响应模型
|
|
93
|
-
|
|
94
|
-
Returns:
|
|
95
|
-
响应对象
|
|
96
|
-
"""
|
|
97
|
-
if not HTTPX_AVAILABLE:
|
|
98
|
-
raise RuntimeError("httpx library is required for async requests. Install it with: pip install httpx")
|
|
99
|
-
|
|
100
|
-
url = f"{self._base_url}/api/{method_name}"
|
|
101
|
-
headers = {}
|
|
102
|
-
if self._token:
|
|
103
|
-
headers["Authorization"] = f"Bearer {self._token}"
|
|
104
|
-
|
|
105
|
-
# 懒加载创建async client
|
|
106
|
-
if self._async_client is None:
|
|
107
|
-
self._async_client = httpx.AsyncClient(timeout=30.0)
|
|
108
|
-
|
|
109
|
-
try:
|
|
110
|
-
# 发送异步post请求
|
|
111
|
-
response = await self._async_client.post(
|
|
112
|
-
url,
|
|
113
|
-
json=request_data.model_dump(),
|
|
114
|
-
headers=headers or None
|
|
115
|
-
)
|
|
116
|
-
# 检查响应状态码
|
|
117
|
-
if not response.is_success:
|
|
118
|
-
try:
|
|
119
|
-
error_data = response.json()
|
|
120
|
-
error_message = error_data.get("error", response.text)
|
|
121
|
-
except ValueError:
|
|
122
|
-
error_message = response.text
|
|
123
|
-
raise httpx.HTTPStatusError(
|
|
124
|
-
f"HTTP error occurred: {response.status_code} - {error_message}",
|
|
125
|
-
request=response.request,
|
|
126
|
-
response=response
|
|
127
|
-
)
|
|
128
|
-
# 返回请求结果
|
|
129
|
-
return response_model(**response.json())
|
|
130
|
-
except httpx.HTTPStatusError:
|
|
131
|
-
raise
|
|
132
|
-
except Exception as e:
|
|
133
|
-
self._logger.error(f"An error occurred: {e}")
|
|
134
|
-
raise
|
|
135
|
-
|
|
136
|
-
# 回调方式发送请求
|
|
137
|
-
def _make_request_callback(
|
|
138
|
-
self,
|
|
139
|
-
method_name: str,
|
|
140
|
-
request_data: BaseModel,
|
|
141
|
-
response_model: Type[R],
|
|
142
|
-
on_success: Optional[Callable[[R], None]] = None,
|
|
143
|
-
on_error: Optional[Callable[[Exception], None]] = None
|
|
144
|
-
) -> None:
|
|
145
|
-
"""
|
|
146
|
-
回调方式发送请求,使用共享的 event loop
|
|
147
|
-
|
|
148
|
-
Args:
|
|
149
|
-
method_name: API方法名
|
|
150
|
-
request_data: 请求数据
|
|
151
|
-
response_model: 响应模型
|
|
152
|
-
on_success: 成功回调,接收响应对象,可选
|
|
153
|
-
on_error: 错误回调,接收异常对象,可选
|
|
154
|
-
"""
|
|
155
|
-
# 确保共享 loop 已启动
|
|
156
|
-
self._ensure_shared_loop()
|
|
157
|
-
|
|
158
|
-
async def _async_task():
|
|
159
|
-
"""异步任务"""
|
|
160
|
-
try:
|
|
161
|
-
result = await self._make_request_async(method_name, request_data, response_model)
|
|
162
|
-
# 调用成功回调(如果提供)
|
|
163
|
-
if on_success:
|
|
164
|
-
on_success(result)
|
|
165
|
-
except Exception as e:
|
|
166
|
-
# 调用错误回调
|
|
167
|
-
if on_error:
|
|
168
|
-
on_error(e)
|
|
169
|
-
else:
|
|
170
|
-
self._logger.error(f"Callback request failed: {e}")
|
|
171
|
-
|
|
172
|
-
# 将任务提交到共享的 event loop
|
|
173
|
-
asyncio.run_coroutine_threadsafe(_async_task(), self._shared_loop)
|
|
174
|
-
|
|
175
|
-
async def close_async(self):
|
|
176
|
-
"""关闭异步客户端连接"""
|
|
177
|
-
if self._async_client is not None:
|
|
178
|
-
await self._async_client.aclose()
|
|
179
|
-
self._async_client = None
|
|
180
|
-
|
|
181
|
-
async def __aenter__(self):
|
|
182
|
-
"""异步上下文管理器入口"""
|
|
183
|
-
return self
|
|
184
|
-
|
|
185
|
-
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
186
|
-
"""异步上下文管理器出口"""
|
|
187
|
-
await self.close_async()
|
|
1
|
+
from typing import Optional, Type, TypeVar, Callable
|
|
2
|
+
import asyncio
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
try:
|
|
8
|
+
import httpx
|
|
9
|
+
HTTPX_AVAILABLE = True
|
|
10
|
+
except ImportError:
|
|
11
|
+
HTTPX_AVAILABLE = False
|
|
12
|
+
|
|
13
|
+
from kaq_quant_common.api.common.auth import get_auth_token
|
|
14
|
+
from kaq_quant_common.utils import logger_utils
|
|
15
|
+
from pydantic import BaseModel
|
|
16
|
+
|
|
17
|
+
R = TypeVar("R", bound=BaseModel)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class ApiClientBase:
|
|
21
|
+
"""
|
|
22
|
+
api 客户端
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
# 类级别的共享 event loop 线程
|
|
26
|
+
_shared_loop: Optional[asyncio.AbstractEventLoop] = None
|
|
27
|
+
_shared_loop_thread: Optional[threading.Thread] = None
|
|
28
|
+
_loop_lock = threading.Lock()
|
|
29
|
+
|
|
30
|
+
def __init__(self, base_url: str, token: Optional[str] = None):
|
|
31
|
+
self._base_url = base_url.rstrip("/")
|
|
32
|
+
self._token = token if token is not None else get_auth_token()
|
|
33
|
+
self._logger = logger_utils.get_logger(self)
|
|
34
|
+
# 异步客户端(懒加载)
|
|
35
|
+
self._async_client: Optional[httpx.AsyncClient] = None
|
|
36
|
+
|
|
37
|
+
@classmethod
|
|
38
|
+
def _ensure_shared_loop(cls):
|
|
39
|
+
"""确保共享的 event loop 已创建并运行"""
|
|
40
|
+
if cls._shared_loop is None or not cls._shared_loop.is_running():
|
|
41
|
+
with cls._loop_lock:
|
|
42
|
+
# 双重检查
|
|
43
|
+
if cls._shared_loop is None or not cls._shared_loop.is_running():
|
|
44
|
+
def run_loop():
|
|
45
|
+
"""在后台线程中运行 event loop"""
|
|
46
|
+
cls._shared_loop = asyncio.new_event_loop()
|
|
47
|
+
asyncio.set_event_loop(cls._shared_loop)
|
|
48
|
+
cls._shared_loop.run_forever()
|
|
49
|
+
|
|
50
|
+
cls._shared_loop_thread = threading.Thread(
|
|
51
|
+
target=run_loop,
|
|
52
|
+
daemon=True,
|
|
53
|
+
name="ApiClient-EventLoop"
|
|
54
|
+
)
|
|
55
|
+
cls._shared_loop_thread.start()
|
|
56
|
+
|
|
57
|
+
# 等待 loop 启动
|
|
58
|
+
while cls._shared_loop is None:
|
|
59
|
+
time.sleep(0.01)
|
|
60
|
+
|
|
61
|
+
# 发送请求
|
|
62
|
+
def _make_request(self, method_name: str, request_data: BaseModel, response_model: Type[R]) -> R:
|
|
63
|
+
url = f"{self._base_url}/api/{method_name}"
|
|
64
|
+
headers = {}
|
|
65
|
+
if self._token:
|
|
66
|
+
headers["Authorization"] = f"Bearer {self._token}"
|
|
67
|
+
try:
|
|
68
|
+
# 发送post请求
|
|
69
|
+
response = requests.post(url, json=request_data.model_dump(), headers=headers or None)
|
|
70
|
+
# 检查响应状态码,如果不成功,则尝试解析错误信息并抛出异常
|
|
71
|
+
if not response.ok:
|
|
72
|
+
try:
|
|
73
|
+
error_data = response.json()
|
|
74
|
+
error_message = error_data.get("error", response.text)
|
|
75
|
+
except ValueError:
|
|
76
|
+
error_message = response.text
|
|
77
|
+
raise requests.exceptions.HTTPError(f"HTTP error occurred: {response.status_code} - {error_message}", response=response)
|
|
78
|
+
# 返回请求结果
|
|
79
|
+
return response_model(**response.json())
|
|
80
|
+
except requests.exceptions.RequestException as e:
|
|
81
|
+
self._logger.error(f"An error occurred: {e}")
|
|
82
|
+
raise
|
|
83
|
+
|
|
84
|
+
# 异步发送请求
|
|
85
|
+
async def _make_request_async(self, method_name: str, request_data: BaseModel, response_model: Type[R]) -> R:
|
|
86
|
+
"""
|
|
87
|
+
异步发送请求
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
method_name: API方法名
|
|
91
|
+
request_data: 请求数据
|
|
92
|
+
response_model: 响应模型
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
响应对象
|
|
96
|
+
"""
|
|
97
|
+
if not HTTPX_AVAILABLE:
|
|
98
|
+
raise RuntimeError("httpx library is required for async requests. Install it with: pip install httpx")
|
|
99
|
+
|
|
100
|
+
url = f"{self._base_url}/api/{method_name}"
|
|
101
|
+
headers = {}
|
|
102
|
+
if self._token:
|
|
103
|
+
headers["Authorization"] = f"Bearer {self._token}"
|
|
104
|
+
|
|
105
|
+
# 懒加载创建async client
|
|
106
|
+
if self._async_client is None:
|
|
107
|
+
self._async_client = httpx.AsyncClient(timeout=30.0)
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
# 发送异步post请求
|
|
111
|
+
response = await self._async_client.post(
|
|
112
|
+
url,
|
|
113
|
+
json=request_data.model_dump(),
|
|
114
|
+
headers=headers or None
|
|
115
|
+
)
|
|
116
|
+
# 检查响应状态码
|
|
117
|
+
if not response.is_success:
|
|
118
|
+
try:
|
|
119
|
+
error_data = response.json()
|
|
120
|
+
error_message = error_data.get("error", response.text)
|
|
121
|
+
except ValueError:
|
|
122
|
+
error_message = response.text
|
|
123
|
+
raise httpx.HTTPStatusError(
|
|
124
|
+
f"HTTP error occurred: {response.status_code} - {error_message}",
|
|
125
|
+
request=response.request,
|
|
126
|
+
response=response
|
|
127
|
+
)
|
|
128
|
+
# 返回请求结果
|
|
129
|
+
return response_model(**response.json())
|
|
130
|
+
except httpx.HTTPStatusError:
|
|
131
|
+
raise
|
|
132
|
+
except Exception as e:
|
|
133
|
+
self._logger.error(f"An error occurred: {e}")
|
|
134
|
+
raise
|
|
135
|
+
|
|
136
|
+
# 回调方式发送请求
|
|
137
|
+
def _make_request_callback(
|
|
138
|
+
self,
|
|
139
|
+
method_name: str,
|
|
140
|
+
request_data: BaseModel,
|
|
141
|
+
response_model: Type[R],
|
|
142
|
+
on_success: Optional[Callable[[R], None]] = None,
|
|
143
|
+
on_error: Optional[Callable[[Exception], None]] = None
|
|
144
|
+
) -> None:
|
|
145
|
+
"""
|
|
146
|
+
回调方式发送请求,使用共享的 event loop
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
method_name: API方法名
|
|
150
|
+
request_data: 请求数据
|
|
151
|
+
response_model: 响应模型
|
|
152
|
+
on_success: 成功回调,接收响应对象,可选
|
|
153
|
+
on_error: 错误回调,接收异常对象,可选
|
|
154
|
+
"""
|
|
155
|
+
# 确保共享 loop 已启动
|
|
156
|
+
self._ensure_shared_loop()
|
|
157
|
+
|
|
158
|
+
async def _async_task():
|
|
159
|
+
"""异步任务"""
|
|
160
|
+
try:
|
|
161
|
+
result = await self._make_request_async(method_name, request_data, response_model)
|
|
162
|
+
# 调用成功回调(如果提供)
|
|
163
|
+
if on_success:
|
|
164
|
+
on_success(result)
|
|
165
|
+
except Exception as e:
|
|
166
|
+
# 调用错误回调
|
|
167
|
+
if on_error:
|
|
168
|
+
on_error(e)
|
|
169
|
+
else:
|
|
170
|
+
self._logger.error(f"Callback request failed: {e}")
|
|
171
|
+
|
|
172
|
+
# 将任务提交到共享的 event loop
|
|
173
|
+
asyncio.run_coroutine_threadsafe(_async_task(), self._shared_loop)
|
|
174
|
+
|
|
175
|
+
async def close_async(self):
|
|
176
|
+
"""关闭异步客户端连接"""
|
|
177
|
+
if self._async_client is not None:
|
|
178
|
+
await self._async_client.aclose()
|
|
179
|
+
self._async_client = None
|
|
180
|
+
|
|
181
|
+
async def __aenter__(self):
|
|
182
|
+
"""异步上下文管理器入口"""
|
|
183
|
+
return self
|
|
184
|
+
|
|
185
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
|
186
|
+
"""异步上下文管理器出口"""
|
|
187
|
+
await self.close_async()
|