ErisPulse 1.2.9__py3-none-any.whl → 2.1.1__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.
- ErisPulse/Core/__init__.py +19 -0
- ErisPulse/Core/adapter.py +548 -0
- ErisPulse/Core/env.py +614 -0
- ErisPulse/{logger.py → Core/logger.py} +51 -113
- ErisPulse/Core/mods.py +226 -0
- ErisPulse/Core/raiserr.py +152 -0
- ErisPulse/Core/server.py +276 -0
- ErisPulse/Core/shellprint.py +165 -0
- ErisPulse/Core/util.py +126 -0
- ErisPulse/__init__.py +673 -243
- ErisPulse/__main__.py +391 -1184
- {erispulse-1.2.9.dist-info → erispulse-2.1.1.dist-info}/METADATA +16 -41
- erispulse-2.1.1.dist-info/RECORD +16 -0
- {erispulse-1.2.9.dist-info → erispulse-2.1.1.dist-info}/entry_points.txt +1 -0
- {erispulse-1.2.9.dist-info → erispulse-2.1.1.dist-info}/licenses/LICENSE +4 -3
- ErisPulse/adapter.py +0 -465
- ErisPulse/db.py +0 -769
- ErisPulse/mods.py +0 -345
- ErisPulse/raiserr.py +0 -141
- ErisPulse/util.py +0 -144
- erispulse-1.2.9.dist-info/RECORD +0 -13
- {erispulse-1.2.9.dist-info → erispulse-2.1.1.dist-info}/WHEEL +0 -0
ErisPulse/Core/server.py
ADDED
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ErisPulse Adapter Server
|
|
3
|
+
提供统一的适配器服务入口,支持HTTP和WebSocket路由
|
|
4
|
+
|
|
5
|
+
{!--< tips >!--}
|
|
6
|
+
1. 适配器只需注册路由,无需自行管理服务器
|
|
7
|
+
2. WebSocket支持自定义认证逻辑
|
|
8
|
+
3. 兼容FastAPI 0.68+ 版本
|
|
9
|
+
{!--< /tips >!--}
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
|
|
13
|
+
from fastapi.routing import APIRoute
|
|
14
|
+
from typing import Dict, List, Optional, Callable, Any, Awaitable, Tuple
|
|
15
|
+
from collections import defaultdict
|
|
16
|
+
from .logger import logger
|
|
17
|
+
import asyncio
|
|
18
|
+
from hypercorn.config import Config
|
|
19
|
+
from hypercorn.asyncio import serve
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AdapterServer:
|
|
23
|
+
"""
|
|
24
|
+
适配器服务器管理器
|
|
25
|
+
|
|
26
|
+
{!--< tips >!--}
|
|
27
|
+
核心功能:
|
|
28
|
+
- HTTP/WebSocket路由注册
|
|
29
|
+
- 生命周期管理
|
|
30
|
+
- 统一错误处理
|
|
31
|
+
{!--< /tips >!--}
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self):
|
|
35
|
+
"""
|
|
36
|
+
初始化适配器服务器
|
|
37
|
+
|
|
38
|
+
{!--< tips >!--}
|
|
39
|
+
会自动创建FastAPI实例并设置核心路由
|
|
40
|
+
{!--< /tips >!--}
|
|
41
|
+
"""
|
|
42
|
+
self.app = FastAPI(
|
|
43
|
+
title="ErisPulse Adapter Server",
|
|
44
|
+
description="统一适配器服务入口点",
|
|
45
|
+
version="1.0.0"
|
|
46
|
+
)
|
|
47
|
+
self._webhook_routes: Dict[str, Dict[str, Callable]] = defaultdict(dict)
|
|
48
|
+
self._websocket_routes: Dict[str, Dict[str, Tuple[Callable, Optional[Callable]]]] = defaultdict(dict)
|
|
49
|
+
self.base_url = ""
|
|
50
|
+
self._server_task: Optional[asyncio.Task] = None
|
|
51
|
+
self._setup_core_routes()
|
|
52
|
+
|
|
53
|
+
def _setup_core_routes(self) -> None:
|
|
54
|
+
"""
|
|
55
|
+
设置系统核心路由
|
|
56
|
+
|
|
57
|
+
{!--< internal-use >!--}
|
|
58
|
+
此方法仅供内部使用
|
|
59
|
+
{!--< /internal-use >!--}
|
|
60
|
+
"""
|
|
61
|
+
@self.app.get("/health")
|
|
62
|
+
async def health_check() -> Dict[str, str]:
|
|
63
|
+
"""
|
|
64
|
+
健康检查端点
|
|
65
|
+
|
|
66
|
+
:return:
|
|
67
|
+
Dict[str, str]: 包含服务状态的字典
|
|
68
|
+
"""
|
|
69
|
+
return {"status": "ok", "service": "ErisPulse Adapter Server"}
|
|
70
|
+
|
|
71
|
+
@self.app.get("/routes")
|
|
72
|
+
async def list_routes() -> Dict[str, Any]:
|
|
73
|
+
"""
|
|
74
|
+
列出所有已注册路由
|
|
75
|
+
|
|
76
|
+
:return:
|
|
77
|
+
Dict[str, Any]: 包含所有路由信息的字典,格式为:
|
|
78
|
+
{
|
|
79
|
+
"http_routes": [
|
|
80
|
+
{
|
|
81
|
+
"path": "/adapter1/route1",
|
|
82
|
+
"adapter": "adapter1",
|
|
83
|
+
"methods": ["POST"]
|
|
84
|
+
},
|
|
85
|
+
...
|
|
86
|
+
],
|
|
87
|
+
"websocket_routes": [
|
|
88
|
+
{
|
|
89
|
+
"path": "/adapter1/ws",
|
|
90
|
+
"adapter": "adapter1",
|
|
91
|
+
"requires_auth": true
|
|
92
|
+
},
|
|
93
|
+
...
|
|
94
|
+
],
|
|
95
|
+
"base_url": self.base_url
|
|
96
|
+
}
|
|
97
|
+
"""
|
|
98
|
+
http_routes = []
|
|
99
|
+
for adapter, routes in self._webhook_routes.items():
|
|
100
|
+
for path, handler in routes.items():
|
|
101
|
+
route = self.app.router.routes[-1] # 获取最后添加的路由
|
|
102
|
+
if isinstance(route, APIRoute) and route.path == path:
|
|
103
|
+
http_routes.append({
|
|
104
|
+
"path": path,
|
|
105
|
+
"adapter": adapter,
|
|
106
|
+
"methods": route.methods
|
|
107
|
+
})
|
|
108
|
+
|
|
109
|
+
websocket_routes = []
|
|
110
|
+
for adapter, routes in self._websocket_routes.items():
|
|
111
|
+
for path, (handler, auth_handler) in routes.items():
|
|
112
|
+
websocket_routes.append({
|
|
113
|
+
"path": path,
|
|
114
|
+
"adapter": adapter,
|
|
115
|
+
"requires_auth": auth_handler is not None
|
|
116
|
+
})
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
"http_routes": http_routes,
|
|
120
|
+
"websocket_routes": websocket_routes,
|
|
121
|
+
"base_url": self.base_url
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
def register_webhook(
|
|
125
|
+
self,
|
|
126
|
+
adapter_name: str,
|
|
127
|
+
path: str,
|
|
128
|
+
handler: Callable,
|
|
129
|
+
methods: List[str] = ["POST"]
|
|
130
|
+
) -> None:
|
|
131
|
+
"""
|
|
132
|
+
注册HTTP路由
|
|
133
|
+
|
|
134
|
+
:param adapter_name: str 适配器名称
|
|
135
|
+
:param path: str 路由路径(如"/message")
|
|
136
|
+
:param handler: Callable 处理函数
|
|
137
|
+
:param methods: List[str] HTTP方法列表(默认["POST"])
|
|
138
|
+
|
|
139
|
+
:raises ValueError: 当路径已注册时抛出
|
|
140
|
+
|
|
141
|
+
{!--< tips >!--}
|
|
142
|
+
路径会自动添加适配器前缀,如:/adapter_name/path
|
|
143
|
+
{!--< /tips >!--}
|
|
144
|
+
"""
|
|
145
|
+
full_path = f"/{adapter_name}{path}"
|
|
146
|
+
|
|
147
|
+
if full_path in self._webhook_routes[adapter_name]:
|
|
148
|
+
raise ValueError(f"路径 {full_path} 已注册")
|
|
149
|
+
|
|
150
|
+
route = APIRoute(
|
|
151
|
+
path=full_path,
|
|
152
|
+
endpoint=handler,
|
|
153
|
+
methods=methods,
|
|
154
|
+
name=f"{adapter_name}{path}"
|
|
155
|
+
)
|
|
156
|
+
self.app.router.routes.append(route)
|
|
157
|
+
self._webhook_routes[adapter_name][full_path] = handler
|
|
158
|
+
logger.info(f"注册HTTP路由: {self.base_url}{full_path} 方法: {methods}")
|
|
159
|
+
|
|
160
|
+
def register_websocket(
|
|
161
|
+
self,
|
|
162
|
+
adapter_name: str,
|
|
163
|
+
path: str,
|
|
164
|
+
handler: Callable[[WebSocket], Awaitable[Any]],
|
|
165
|
+
auth_handler: Optional[Callable[[WebSocket], Awaitable[bool]]] = None,
|
|
166
|
+
) -> None:
|
|
167
|
+
"""
|
|
168
|
+
注册WebSocket路由
|
|
169
|
+
|
|
170
|
+
:param adapter_name: str 适配器名称
|
|
171
|
+
:param path: str WebSocket路径(如"/ws")
|
|
172
|
+
:param handler: Callable[[WebSocket], Awaitable[Any]] 主处理函数
|
|
173
|
+
:param auth_handler: Optional[Callable[[WebSocket], Awaitable[bool]]] 认证函数
|
|
174
|
+
|
|
175
|
+
:raises ValueError: 当路径已注册时抛出
|
|
176
|
+
|
|
177
|
+
{!--< tips >!--}
|
|
178
|
+
认证函数应返回布尔值,False将拒绝连接
|
|
179
|
+
{!--< /tips >!--}
|
|
180
|
+
"""
|
|
181
|
+
full_path = f"/{adapter_name}{path}"
|
|
182
|
+
|
|
183
|
+
if full_path in self._websocket_routes[adapter_name]:
|
|
184
|
+
raise ValueError(f"WebSocket路径 {full_path} 已注册")
|
|
185
|
+
|
|
186
|
+
async def websocket_endpoint(websocket: WebSocket) -> None:
|
|
187
|
+
"""
|
|
188
|
+
WebSocket端点包装器
|
|
189
|
+
|
|
190
|
+
{!--< internal-use >!--}
|
|
191
|
+
处理连接生命周期和错误处理
|
|
192
|
+
{!--< /internal-use >!--}
|
|
193
|
+
"""
|
|
194
|
+
await websocket.accept()
|
|
195
|
+
|
|
196
|
+
try:
|
|
197
|
+
if auth_handler and not await auth_handler(websocket):
|
|
198
|
+
await websocket.close(code=1008)
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
await handler(websocket)
|
|
202
|
+
|
|
203
|
+
except WebSocketDisconnect:
|
|
204
|
+
logger.debug(f"客户端断开: {full_path}")
|
|
205
|
+
except Exception as e:
|
|
206
|
+
logger.error(f"WebSocket错误: {e}")
|
|
207
|
+
await websocket.close(code=1011)
|
|
208
|
+
|
|
209
|
+
self.app.add_api_websocket_route(
|
|
210
|
+
path=full_path,
|
|
211
|
+
endpoint=websocket_endpoint,
|
|
212
|
+
name=f"{adapter_name}{path}"
|
|
213
|
+
)
|
|
214
|
+
self._websocket_routes[adapter_name][full_path] = (handler, auth_handler)
|
|
215
|
+
logger.info(f"注册WebSocket: {self.base_url}{full_path} {'(需认证)' if auth_handler else ''}")
|
|
216
|
+
|
|
217
|
+
def get_app(self) -> FastAPI:
|
|
218
|
+
"""
|
|
219
|
+
获取FastAPI应用实例
|
|
220
|
+
|
|
221
|
+
:return:
|
|
222
|
+
FastAPI: FastAPI应用实例
|
|
223
|
+
"""
|
|
224
|
+
return self.app
|
|
225
|
+
|
|
226
|
+
async def start(
|
|
227
|
+
self,
|
|
228
|
+
host: str = "0.0.0.0",
|
|
229
|
+
port: int = 8000,
|
|
230
|
+
ssl_certfile: Optional[str] = None,
|
|
231
|
+
ssl_keyfile: Optional[str] = None
|
|
232
|
+
) -> None:
|
|
233
|
+
"""
|
|
234
|
+
启动适配器服务器
|
|
235
|
+
|
|
236
|
+
:param host: str 监听地址(默认"0.0.0.0")
|
|
237
|
+
:param port: int 监听端口(默认8000)
|
|
238
|
+
:param ssl_certfile: Optional[str] SSL证书路径
|
|
239
|
+
:param ssl_keyfile: Optional[str] SSL密钥路径
|
|
240
|
+
|
|
241
|
+
:raises RuntimeError: 当服务器已在运行时抛出
|
|
242
|
+
"""
|
|
243
|
+
if self._server_task and not self._server_task.done():
|
|
244
|
+
raise RuntimeError("服务器已在运行中")
|
|
245
|
+
|
|
246
|
+
config = Config()
|
|
247
|
+
config.bind = [f"{host}:{port}"]
|
|
248
|
+
# config.loglevel = "debug"
|
|
249
|
+
|
|
250
|
+
if ssl_certfile and ssl_keyfile:
|
|
251
|
+
config.certfile = ssl_certfile
|
|
252
|
+
config.keyfile = ssl_keyfile
|
|
253
|
+
|
|
254
|
+
self.base_url = f"http{'s' if ssl_certfile else ''}://{host}:{port}"
|
|
255
|
+
logger.info(f"启动服务器 {self.base_url}")
|
|
256
|
+
|
|
257
|
+
self._server_task = asyncio.create_task(serve(self.app, config))
|
|
258
|
+
|
|
259
|
+
async def stop(self) -> None:
|
|
260
|
+
"""
|
|
261
|
+
停止服务器
|
|
262
|
+
|
|
263
|
+
{!--< tips >!--}
|
|
264
|
+
会等待所有连接正常关闭
|
|
265
|
+
{!--< /tips >!--}
|
|
266
|
+
"""
|
|
267
|
+
if self._server_task:
|
|
268
|
+
self._server_task.cancel()
|
|
269
|
+
try:
|
|
270
|
+
await self._server_task
|
|
271
|
+
except asyncio.CancelledError:
|
|
272
|
+
logger.info("服务器已停止")
|
|
273
|
+
self._server_task = None
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
adapter_server = AdapterServer()
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
class Shell_Printer:
|
|
4
|
+
# ANSI 颜色代码
|
|
5
|
+
RESET = "\033[0m"
|
|
6
|
+
BOLD = "\033[1m"
|
|
7
|
+
RED = "\033[91m"
|
|
8
|
+
GREEN = "\033[92m"
|
|
9
|
+
YELLOW = "\033[93m"
|
|
10
|
+
BLUE = "\033[94m"
|
|
11
|
+
MAGENTA = "\033[95m"
|
|
12
|
+
CYAN = "\033[96m"
|
|
13
|
+
WHITE = "\033[97m"
|
|
14
|
+
DIM = "\033[2m"
|
|
15
|
+
UNDERLINE = "\033[4m"
|
|
16
|
+
BG_BLUE = "\033[44m"
|
|
17
|
+
BG_GREEN = "\033[42m"
|
|
18
|
+
BG_YELLOW = "\033[43m"
|
|
19
|
+
BG_RED = "\033[41m"
|
|
20
|
+
|
|
21
|
+
def __init__(self):
|
|
22
|
+
pass
|
|
23
|
+
|
|
24
|
+
@classmethod
|
|
25
|
+
def _get_color(cls, level):
|
|
26
|
+
return {
|
|
27
|
+
"info": cls.CYAN,
|
|
28
|
+
"success": cls.GREEN,
|
|
29
|
+
"warning": cls.YELLOW,
|
|
30
|
+
"error": cls.RED,
|
|
31
|
+
"title": cls.MAGENTA,
|
|
32
|
+
"default": cls.RESET,
|
|
33
|
+
}.get(level, cls.RESET)
|
|
34
|
+
|
|
35
|
+
@classmethod
|
|
36
|
+
def panel(cls, msg: str, title: str = None, level: str = "info") -> None:
|
|
37
|
+
color = cls._get_color(level)
|
|
38
|
+
width = 70
|
|
39
|
+
border_char = "─" * width
|
|
40
|
+
|
|
41
|
+
if level == "error":
|
|
42
|
+
border_char = "═" * width
|
|
43
|
+
msg = f"{cls.RED}✗ {msg}{cls.RESET}"
|
|
44
|
+
elif level == "warning":
|
|
45
|
+
border_char = "─" * width
|
|
46
|
+
msg = f"{cls.YELLOW}⚠ {msg}{cls.RESET}"
|
|
47
|
+
|
|
48
|
+
title_line = ""
|
|
49
|
+
if title:
|
|
50
|
+
title = f" {title.upper()} "
|
|
51
|
+
title_padding = (width - len(title)) // 2
|
|
52
|
+
left_pad = " " * title_padding
|
|
53
|
+
right_pad = " " * (width - len(title) - title_padding)
|
|
54
|
+
title_line = f"{cls.DIM}┌{left_pad}{cls.BOLD}{color}{title}{cls.RESET}{cls.DIM}{right_pad}┐{cls.RESET}\n"
|
|
55
|
+
|
|
56
|
+
lines = []
|
|
57
|
+
for line in msg.split("\n"):
|
|
58
|
+
if len(line) > width - 4:
|
|
59
|
+
words = line.split()
|
|
60
|
+
current_line = ""
|
|
61
|
+
for word in words:
|
|
62
|
+
if len(current_line) + len(word) + 1 > width - 4:
|
|
63
|
+
lines.append(f"{cls.DIM}│{cls.RESET} {current_line.ljust(width-4)} {cls.DIM}│{cls.RESET}")
|
|
64
|
+
current_line = word
|
|
65
|
+
else:
|
|
66
|
+
current_line += (" " + word) if current_line else word
|
|
67
|
+
if current_line:
|
|
68
|
+
lines.append(f"{cls.DIM}│{cls.RESET} {current_line.ljust(width-4)} {cls.DIM}│{cls.RESET}")
|
|
69
|
+
else:
|
|
70
|
+
lines.append(f"{cls.DIM}│{cls.RESET} {line.ljust(width-4)} {cls.DIM}│{cls.RESET}")
|
|
71
|
+
|
|
72
|
+
if level == "error":
|
|
73
|
+
border_style = "╘"
|
|
74
|
+
elif level == "warning":
|
|
75
|
+
border_style = "╧"
|
|
76
|
+
else:
|
|
77
|
+
border_style = "└"
|
|
78
|
+
bottom_border = f"{cls.DIM}{border_style}{border_char}┘{cls.RESET}"
|
|
79
|
+
|
|
80
|
+
panel = f"{title_line}"
|
|
81
|
+
panel += f"{cls.DIM}├{border_char}┤{cls.RESET}\n"
|
|
82
|
+
panel += "\n".join(lines) + "\n"
|
|
83
|
+
panel += f"{bottom_border}\n"
|
|
84
|
+
|
|
85
|
+
print(panel)
|
|
86
|
+
|
|
87
|
+
@classmethod
|
|
88
|
+
def table(cls, headers, rows, title=None, level="info") -> None:
|
|
89
|
+
color = cls._get_color(level)
|
|
90
|
+
if title:
|
|
91
|
+
print(f"{cls.BOLD}{color}== {title} =={cls.RESET}")
|
|
92
|
+
|
|
93
|
+
col_widths = [len(h) for h in headers]
|
|
94
|
+
for row in rows:
|
|
95
|
+
for i, cell in enumerate(row):
|
|
96
|
+
col_widths[i] = max(col_widths[i], len(str(cell)))
|
|
97
|
+
|
|
98
|
+
fmt = "│".join(f" {{:<{w}}} " for w in col_widths)
|
|
99
|
+
|
|
100
|
+
top_border = "┌" + "┬".join("─" * (w+2) for w in col_widths) + "┐"
|
|
101
|
+
print(f"{cls.DIM}{top_border}{cls.RESET}")
|
|
102
|
+
|
|
103
|
+
header_line = fmt.format(*headers)
|
|
104
|
+
print(f"{cls.BOLD}{color}│{header_line}│{cls.RESET}")
|
|
105
|
+
|
|
106
|
+
separator = "├" + "┼".join("─" * (w+2) for w in col_widths) + "┤"
|
|
107
|
+
print(f"{cls.DIM}{separator}{cls.RESET}")
|
|
108
|
+
|
|
109
|
+
for row in rows:
|
|
110
|
+
row_line = fmt.format(*row)
|
|
111
|
+
print(f"│{row_line}│")
|
|
112
|
+
|
|
113
|
+
bottom_border = "└" + "┴".join("─" * (w+2) for w in col_widths) + "┘"
|
|
114
|
+
print(f"{cls.DIM}{bottom_border}{cls.RESET}")
|
|
115
|
+
|
|
116
|
+
@classmethod
|
|
117
|
+
def progress_bar(cls, current, total, prefix="", suffix="", length=50):
|
|
118
|
+
filled_length = int(length * current // total)
|
|
119
|
+
percent = min(100.0, 100 * (current / float(total)))
|
|
120
|
+
bar = f"{cls.GREEN}{'█' * filled_length}{cls.WHITE}{'░' * (length - filled_length)}{cls.RESET}"
|
|
121
|
+
sys.stdout.write(f"\r{cls.BOLD}{prefix}{cls.RESET} {bar} {cls.BOLD}{percent:.1f}%{cls.RESET} {suffix}")
|
|
122
|
+
sys.stdout.flush()
|
|
123
|
+
if current == total:
|
|
124
|
+
print()
|
|
125
|
+
|
|
126
|
+
@classmethod
|
|
127
|
+
def confirm(cls, msg, default=False) -> bool:
|
|
128
|
+
yes_options = {'y', 'yes'}
|
|
129
|
+
no_options = {'n', 'no'}
|
|
130
|
+
default_str = "Y/n" if default else "y/N"
|
|
131
|
+
prompt = f"{cls.BOLD}{msg}{cls.RESET} [{cls.CYAN}{default_str}{cls.RESET}]: "
|
|
132
|
+
|
|
133
|
+
while True:
|
|
134
|
+
ans = input(prompt).strip().lower()
|
|
135
|
+
if not ans:
|
|
136
|
+
return default
|
|
137
|
+
if ans in yes_options:
|
|
138
|
+
return True
|
|
139
|
+
if ans in no_options:
|
|
140
|
+
return False
|
|
141
|
+
print(f"{cls.YELLOW}请输入 'y' 或 'n'{cls.RESET}")
|
|
142
|
+
|
|
143
|
+
@classmethod
|
|
144
|
+
def ask(cls, msg, choices=None, default="") -> str:
|
|
145
|
+
prompt = f"{cls.BOLD}{msg}{cls.RESET}"
|
|
146
|
+
if choices:
|
|
147
|
+
prompt += f" ({cls.CYAN}{'/'.join(choices)}{cls.RESET})"
|
|
148
|
+
if default:
|
|
149
|
+
prompt += f" [{cls.BLUE}默认: {default}{cls.RESET}]"
|
|
150
|
+
prompt += ": "
|
|
151
|
+
|
|
152
|
+
while True:
|
|
153
|
+
ans = input(prompt).strip()
|
|
154
|
+
if not ans and default:
|
|
155
|
+
return default
|
|
156
|
+
if not choices or ans in choices:
|
|
157
|
+
return ans
|
|
158
|
+
print(f"{cls.YELLOW}请输入有效选项: {', '.join(choices)}{cls.RESET}")
|
|
159
|
+
|
|
160
|
+
@classmethod
|
|
161
|
+
def status(cls, msg, success=True):
|
|
162
|
+
symbol = f"{cls.GREEN}✓" if success else f"{cls.RED}✗"
|
|
163
|
+
print(f"\r{symbol}{cls.RESET} {msg}")
|
|
164
|
+
|
|
165
|
+
shellprint = Shell_Printer()
|
ErisPulse/Core/util.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ErisPulse 工具函数集合
|
|
3
|
+
|
|
4
|
+
提供常用工具函数,包括拓扑排序、缓存装饰器、异步执行等实用功能。
|
|
5
|
+
|
|
6
|
+
{!--< tips >!--}
|
|
7
|
+
1. 使用@cache装饰器缓存函数结果
|
|
8
|
+
2. 使用@run_in_executor在独立线程中运行同步函数
|
|
9
|
+
3. 使用@retry实现自动重试机制
|
|
10
|
+
{!--< /tips >!--}
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
import time
|
|
14
|
+
import asyncio
|
|
15
|
+
import functools
|
|
16
|
+
import traceback
|
|
17
|
+
from concurrent.futures import ThreadPoolExecutor
|
|
18
|
+
from collections import defaultdict, deque
|
|
19
|
+
from typing import List, Dict, Type, Callable, Any, Optional, Set
|
|
20
|
+
|
|
21
|
+
executor = ThreadPoolExecutor()
|
|
22
|
+
|
|
23
|
+
class Util:
|
|
24
|
+
"""
|
|
25
|
+
工具函数集合
|
|
26
|
+
|
|
27
|
+
提供各种实用功能,简化开发流程
|
|
28
|
+
|
|
29
|
+
{!--< tips >!--}
|
|
30
|
+
1. 拓扑排序用于解决依赖关系
|
|
31
|
+
2. 装饰器简化常见模式实现
|
|
32
|
+
3. 异步执行提升性能
|
|
33
|
+
{!--< /tips >!--}
|
|
34
|
+
"""
|
|
35
|
+
def ExecAsync(self, async_func: Callable, *args: Any, **kwargs: Any) -> Any:
|
|
36
|
+
"""
|
|
37
|
+
异步执行函数
|
|
38
|
+
|
|
39
|
+
:param async_func: 异步函数
|
|
40
|
+
:param args: 位置参数
|
|
41
|
+
:param kwargs: 关键字参数
|
|
42
|
+
:return: 函数执行结果
|
|
43
|
+
|
|
44
|
+
:example:
|
|
45
|
+
>>> result = util.ExecAsync(my_async_func, arg1, arg2)
|
|
46
|
+
"""
|
|
47
|
+
loop = asyncio.get_event_loop()
|
|
48
|
+
return loop.run_in_executor(executor, lambda: asyncio.run(async_func(*args, **kwargs)))
|
|
49
|
+
|
|
50
|
+
def cache(self, func: Callable) -> Callable:
|
|
51
|
+
"""
|
|
52
|
+
缓存装饰器
|
|
53
|
+
|
|
54
|
+
:param func: 被装饰函数
|
|
55
|
+
:return: 装饰后的函数
|
|
56
|
+
|
|
57
|
+
:example:
|
|
58
|
+
>>> @util.cache
|
|
59
|
+
>>> def expensive_operation(param):
|
|
60
|
+
>>> return heavy_computation(param)
|
|
61
|
+
"""
|
|
62
|
+
cache_dict = {}
|
|
63
|
+
@functools.wraps(func)
|
|
64
|
+
def wrapper(*args, **kwargs):
|
|
65
|
+
key = (args, tuple(sorted(kwargs.items())))
|
|
66
|
+
if key not in cache_dict:
|
|
67
|
+
cache_dict[key] = func(*args, **kwargs)
|
|
68
|
+
return cache_dict[key]
|
|
69
|
+
return wrapper
|
|
70
|
+
|
|
71
|
+
def run_in_executor(self, func: Callable) -> Callable:
|
|
72
|
+
"""
|
|
73
|
+
在独立线程中执行同步函数的装饰器
|
|
74
|
+
|
|
75
|
+
:param func: 被装饰的同步函数
|
|
76
|
+
:return: 可等待的协程函数
|
|
77
|
+
|
|
78
|
+
:example:
|
|
79
|
+
>>> @util.run_in_executor
|
|
80
|
+
>>> def blocking_io():
|
|
81
|
+
>>> # 执行阻塞IO操作
|
|
82
|
+
>>> return result
|
|
83
|
+
"""
|
|
84
|
+
@functools.wraps(func)
|
|
85
|
+
async def wrapper(*args, **kwargs):
|
|
86
|
+
loop = asyncio.get_event_loop()
|
|
87
|
+
try:
|
|
88
|
+
return await loop.run_in_executor(None, lambda: func(*args, **kwargs))
|
|
89
|
+
except Exception as e:
|
|
90
|
+
from . import logger, raiserr
|
|
91
|
+
logger.error(f"线程内发生未处理异常:\n{''.join(traceback.format_exc())}")
|
|
92
|
+
raiserr.CaughtExternalError(
|
|
93
|
+
f"检测到线程内异常,请优先使用 sdk.raiserr 抛出错误。\n原始异常: {type(e).__name__}: {e}"
|
|
94
|
+
)
|
|
95
|
+
return wrapper
|
|
96
|
+
|
|
97
|
+
def retry(self, max_attempts: int = 3, delay: int = 1) -> Callable:
|
|
98
|
+
"""
|
|
99
|
+
自动重试装饰器
|
|
100
|
+
|
|
101
|
+
:param max_attempts: 最大重试次数 (默认: 3)
|
|
102
|
+
:param delay: 重试间隔(秒) (默认: 1)
|
|
103
|
+
:return: 装饰器函数
|
|
104
|
+
|
|
105
|
+
:example:
|
|
106
|
+
>>> @util.retry(max_attempts=5, delay=2)
|
|
107
|
+
>>> def unreliable_operation():
|
|
108
|
+
>>> # 可能失败的操作
|
|
109
|
+
"""
|
|
110
|
+
def decorator(func: Callable) -> Callable:
|
|
111
|
+
@functools.wraps(func)
|
|
112
|
+
def wrapper(*args, **kwargs):
|
|
113
|
+
attempts = 0
|
|
114
|
+
while attempts < max_attempts:
|
|
115
|
+
try:
|
|
116
|
+
return func(*args, **kwargs)
|
|
117
|
+
except Exception as e:
|
|
118
|
+
attempts += 1
|
|
119
|
+
if attempts == max_attempts:
|
|
120
|
+
raise
|
|
121
|
+
time.sleep(delay)
|
|
122
|
+
return wrapper
|
|
123
|
+
return decorator
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
util = Util()
|