flexllm 0.3.3__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.
- flexllm/__init__.py +224 -0
- flexllm/__main__.py +1096 -0
- flexllm/async_api/__init__.py +9 -0
- flexllm/async_api/concurrent_call.py +100 -0
- flexllm/async_api/concurrent_executor.py +1036 -0
- flexllm/async_api/core.py +373 -0
- flexllm/async_api/interface.py +12 -0
- flexllm/async_api/progress.py +277 -0
- flexllm/base_client.py +988 -0
- flexllm/batch_tools/__init__.py +16 -0
- flexllm/batch_tools/folder_processor.py +317 -0
- flexllm/batch_tools/table_processor.py +363 -0
- flexllm/cache/__init__.py +10 -0
- flexllm/cache/response_cache.py +293 -0
- flexllm/chain_of_thought_client.py +1120 -0
- flexllm/claudeclient.py +402 -0
- flexllm/client_pool.py +698 -0
- flexllm/geminiclient.py +563 -0
- flexllm/llm_client.py +523 -0
- flexllm/llm_parser.py +60 -0
- flexllm/mllm_client.py +559 -0
- flexllm/msg_processors/__init__.py +174 -0
- flexllm/msg_processors/image_processor.py +729 -0
- flexllm/msg_processors/image_processor_helper.py +485 -0
- flexllm/msg_processors/messages_processor.py +341 -0
- flexllm/msg_processors/unified_processor.py +1404 -0
- flexllm/openaiclient.py +256 -0
- flexllm/pricing/__init__.py +104 -0
- flexllm/pricing/data.json +1201 -0
- flexllm/pricing/updater.py +223 -0
- flexllm/provider_router.py +213 -0
- flexllm/token_counter.py +270 -0
- flexllm/utils/__init__.py +1 -0
- flexllm/utils/core.py +41 -0
- flexllm-0.3.3.dist-info/METADATA +573 -0
- flexllm-0.3.3.dist-info/RECORD +39 -0
- flexllm-0.3.3.dist-info/WHEEL +4 -0
- flexllm-0.3.3.dist-info/entry_points.txt +3 -0
- flexllm-0.3.3.dist-info/licenses/LICENSE +201 -0
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
from asyncio import Queue
|
|
3
|
+
import itertools
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from typing import Any, Dict, Iterable, List, Optional, Callable, AsyncIterator, AsyncGenerator, Tuple, Union
|
|
7
|
+
from aiohttp import ClientSession, TCPConnector, ClientTimeout
|
|
8
|
+
from contextlib import asynccontextmanager
|
|
9
|
+
|
|
10
|
+
from ..utils.core import async_retry
|
|
11
|
+
from .interface import RequestResult
|
|
12
|
+
from .progress import ProgressTracker, ProgressBarConfig
|
|
13
|
+
|
|
14
|
+
from dataclasses import dataclass
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@dataclass
|
|
18
|
+
class StreamingResult:
|
|
19
|
+
completed_requests: List[RequestResult]
|
|
20
|
+
progress: Optional[ProgressTracker]
|
|
21
|
+
is_final: bool
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class RateLimiter:
|
|
25
|
+
"""
|
|
26
|
+
速率限制器
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
max_qps: 每秒最大请求数
|
|
30
|
+
use_bucket: 是否使用漏桶算法(aiolimiter),默认 True
|
|
31
|
+
False 时使用简单的锁+sleep 实现
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self, max_qps: Optional[float] = None, use_bucket: bool = True):
|
|
35
|
+
self.max_qps = max_qps
|
|
36
|
+
self._use_bucket = use_bucket
|
|
37
|
+
|
|
38
|
+
if max_qps:
|
|
39
|
+
if use_bucket:
|
|
40
|
+
from aiolimiter import AsyncLimiter
|
|
41
|
+
self._limiter = AsyncLimiter(max_qps, 1)
|
|
42
|
+
else:
|
|
43
|
+
self._lock = asyncio.Lock()
|
|
44
|
+
self._last_request_time = 0
|
|
45
|
+
self._min_interval = 1 / max_qps
|
|
46
|
+
|
|
47
|
+
async def acquire(self):
|
|
48
|
+
if not self.max_qps:
|
|
49
|
+
return
|
|
50
|
+
if self._use_bucket:
|
|
51
|
+
await self._limiter.acquire()
|
|
52
|
+
else:
|
|
53
|
+
async with self._lock:
|
|
54
|
+
elapsed = time.time() - self._last_request_time
|
|
55
|
+
if elapsed < self._min_interval:
|
|
56
|
+
await asyncio.sleep(self._min_interval - elapsed)
|
|
57
|
+
self._last_request_time = time.time()
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class ConcurrentRequester:
|
|
61
|
+
"""
|
|
62
|
+
并发请求管理器
|
|
63
|
+
|
|
64
|
+
Example
|
|
65
|
+
-------
|
|
66
|
+
|
|
67
|
+
requester = ConcurrentRequester(
|
|
68
|
+
concurrency_limit=5,
|
|
69
|
+
max_qps=10,
|
|
70
|
+
timeout=0.7,
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
request_params = [
|
|
74
|
+
{
|
|
75
|
+
'json': {
|
|
76
|
+
'messages': [{"role": "user", "content": "讲个笑话" }],
|
|
77
|
+
'model': "qwen2.5:latest",
|
|
78
|
+
},
|
|
79
|
+
'headers': {'Content-Type': 'application/json'}
|
|
80
|
+
} for i in range(10)
|
|
81
|
+
]
|
|
82
|
+
|
|
83
|
+
# 执行并发请求
|
|
84
|
+
results, tracker = await requester.process_requests(
|
|
85
|
+
request_params=request_params,
|
|
86
|
+
url='http://localhost:11434/v1/chat/completions',
|
|
87
|
+
method='POST',
|
|
88
|
+
show_progress=True
|
|
89
|
+
)
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
def __init__(
|
|
93
|
+
self,
|
|
94
|
+
concurrency_limit: int,
|
|
95
|
+
max_qps: Optional[float] = None,
|
|
96
|
+
timeout: Optional[float] = None,
|
|
97
|
+
retry_times: int = 3,
|
|
98
|
+
retry_delay: float = 0.3
|
|
99
|
+
):
|
|
100
|
+
self._concurrency_limit = concurrency_limit
|
|
101
|
+
if timeout:
|
|
102
|
+
self._timeout = ClientTimeout(total=timeout, connect=min(10., timeout))
|
|
103
|
+
else:
|
|
104
|
+
self._timeout = None
|
|
105
|
+
self._rate_limiter = RateLimiter(max_qps)
|
|
106
|
+
self._semaphore = asyncio.Semaphore(concurrency_limit)
|
|
107
|
+
self.retry_times = retry_times
|
|
108
|
+
self.retry_delay = retry_delay
|
|
109
|
+
|
|
110
|
+
@asynccontextmanager
|
|
111
|
+
async def _get_session(self):
|
|
112
|
+
connector = TCPConnector(limit=self._concurrency_limit+10, limit_per_host=0, force_close=False)
|
|
113
|
+
async with ClientSession(timeout=self._timeout, connector=connector, trust_env=True) as session:
|
|
114
|
+
yield session
|
|
115
|
+
|
|
116
|
+
@staticmethod
|
|
117
|
+
async def _make_requests( session: ClientSession,method: str, url: str, **kwargs):
|
|
118
|
+
async with session.request(method, url, **kwargs) as response:
|
|
119
|
+
response.raise_for_status()
|
|
120
|
+
data = await response.json()
|
|
121
|
+
return response, data
|
|
122
|
+
|
|
123
|
+
async def make_requests(self, session: ClientSession,method: str, url: str, **kwargs):
|
|
124
|
+
return await async_retry(self.retry_times, self.retry_delay)(self._make_requests)(session,method, url, **kwargs)
|
|
125
|
+
|
|
126
|
+
async def _send_single_request(
|
|
127
|
+
self,
|
|
128
|
+
session: ClientSession,
|
|
129
|
+
request_id: int,
|
|
130
|
+
url: str,
|
|
131
|
+
method: str = 'POST',
|
|
132
|
+
meta: dict = None,
|
|
133
|
+
**kwargs
|
|
134
|
+
) -> RequestResult:
|
|
135
|
+
"""发送单个请求"""
|
|
136
|
+
async with self._semaphore:
|
|
137
|
+
try:
|
|
138
|
+
# todo: 速率限制也许需要优化
|
|
139
|
+
await self._rate_limiter.acquire()
|
|
140
|
+
|
|
141
|
+
start_time = time.time()
|
|
142
|
+
response, data = await self.make_requests(session, method, url, **kwargs)
|
|
143
|
+
latency = time.time() - start_time
|
|
144
|
+
|
|
145
|
+
if response.status != 200:
|
|
146
|
+
error_info = {
|
|
147
|
+
'status_code': response.status,
|
|
148
|
+
'response_data': data,
|
|
149
|
+
'error': f"HTTP {response.status}"
|
|
150
|
+
}
|
|
151
|
+
return RequestResult(
|
|
152
|
+
request_id=request_id,
|
|
153
|
+
data=error_info,
|
|
154
|
+
status='error',
|
|
155
|
+
meta=meta,
|
|
156
|
+
latency=latency
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
return RequestResult(
|
|
160
|
+
request_id=request_id,
|
|
161
|
+
data=data,
|
|
162
|
+
status="success",
|
|
163
|
+
meta=meta,
|
|
164
|
+
latency=latency
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
except asyncio.TimeoutError as e:
|
|
168
|
+
return RequestResult(
|
|
169
|
+
request_id=request_id,
|
|
170
|
+
data={'error': 'Timeout error', 'detail': str(e)},
|
|
171
|
+
status='error',
|
|
172
|
+
meta=meta,
|
|
173
|
+
latency=time.time() - start_time
|
|
174
|
+
)
|
|
175
|
+
except Exception as e:
|
|
176
|
+
return RequestResult(
|
|
177
|
+
request_id=request_id,
|
|
178
|
+
data={'error': e.__class__.__name__, 'detail': str(e)},
|
|
179
|
+
status='error',
|
|
180
|
+
meta=meta,
|
|
181
|
+
latency=time.time() - start_time
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
async def process_with_concurrency_window(
|
|
185
|
+
self,
|
|
186
|
+
items: Iterable,
|
|
187
|
+
process_func: Callable,
|
|
188
|
+
concurrency_limit: int,
|
|
189
|
+
progress: Optional[ProgressTracker] = None,
|
|
190
|
+
batch_size: int = 1,
|
|
191
|
+
) -> AsyncGenerator[StreamingResult, Any]:
|
|
192
|
+
"""
|
|
193
|
+
使用滑动窗口方式处理并发任务,支持流式返回结果
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
items: 待处理的项目迭代器
|
|
197
|
+
process_func: 处理单个项目的异步函数,接收item和项目item_id作为参数
|
|
198
|
+
concurrency_limit: 并发限制数量,也是窗口大小
|
|
199
|
+
progress: 可选的进度跟踪器
|
|
200
|
+
batch_size: 每次yield返回的最小完成请求数量
|
|
201
|
+
|
|
202
|
+
Yields:
|
|
203
|
+
生成 StreamingResult 对象序列
|
|
204
|
+
"""
|
|
205
|
+
|
|
206
|
+
async def handle_completed_tasks(done_tasks, batch, is_final=False):
|
|
207
|
+
"""内部函数处理已完成的任务"""
|
|
208
|
+
for task in done_tasks:
|
|
209
|
+
result = await task
|
|
210
|
+
if progress:
|
|
211
|
+
progress.update(result)
|
|
212
|
+
batch.append(result)
|
|
213
|
+
|
|
214
|
+
if len(batch) >= batch_size or (is_final and batch):
|
|
215
|
+
if is_final and progress:
|
|
216
|
+
progress.summary()
|
|
217
|
+
yield StreamingResult(
|
|
218
|
+
completed_requests=sorted(batch, key=lambda x: x.request_id),
|
|
219
|
+
progress=progress,
|
|
220
|
+
is_final=is_final
|
|
221
|
+
)
|
|
222
|
+
batch.clear()
|
|
223
|
+
|
|
224
|
+
item_id = 0
|
|
225
|
+
active_tasks = set()
|
|
226
|
+
completed_batch = []
|
|
227
|
+
|
|
228
|
+
# 处理输入项目
|
|
229
|
+
for item in items:
|
|
230
|
+
if len(active_tasks) >= concurrency_limit:
|
|
231
|
+
done, active_tasks = await asyncio.wait(
|
|
232
|
+
active_tasks,
|
|
233
|
+
return_when=asyncio.FIRST_COMPLETED
|
|
234
|
+
)
|
|
235
|
+
async for result in handle_completed_tasks(done, completed_batch):
|
|
236
|
+
yield result
|
|
237
|
+
|
|
238
|
+
active_tasks.add(asyncio.create_task(process_func(item, item_id)))
|
|
239
|
+
item_id += 1
|
|
240
|
+
|
|
241
|
+
# 处理剩余任务
|
|
242
|
+
if active_tasks:
|
|
243
|
+
done, _ = await asyncio.wait(active_tasks)
|
|
244
|
+
async for result in handle_completed_tasks(done, completed_batch, is_final=True):
|
|
245
|
+
yield result
|
|
246
|
+
|
|
247
|
+
async def _stream_requests(
|
|
248
|
+
self,
|
|
249
|
+
queue: Queue,
|
|
250
|
+
request_params: Iterable[Dict[str, Any]],
|
|
251
|
+
url: str,
|
|
252
|
+
method: str = 'POST',
|
|
253
|
+
total_requests: Optional[int] = None,
|
|
254
|
+
show_progress: bool = True,
|
|
255
|
+
batch_size: Optional[int] = None
|
|
256
|
+
) :
|
|
257
|
+
"""
|
|
258
|
+
流式处理批量请求,实时返回已完成的结果
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
request_params: 请求参数列表
|
|
262
|
+
url: 请求URL
|
|
263
|
+
method: 请求方法
|
|
264
|
+
total_requests: 总请求数量
|
|
265
|
+
show_progress: 是否显示进度
|
|
266
|
+
batch_size: 每次yield返回的最小完成请求数量
|
|
267
|
+
"""
|
|
268
|
+
progress = None
|
|
269
|
+
if batch_size is None:
|
|
270
|
+
batch_size = self._concurrency_limit
|
|
271
|
+
if total_requests is None and show_progress:
|
|
272
|
+
request_params, params_for_counting = itertools.tee(request_params)
|
|
273
|
+
total_requests = sum(1 for _ in params_for_counting)
|
|
274
|
+
|
|
275
|
+
if show_progress and total_requests is not None:
|
|
276
|
+
progress = ProgressTracker(
|
|
277
|
+
total_requests,
|
|
278
|
+
concurrency=self._concurrency_limit,
|
|
279
|
+
config=ProgressBarConfig()
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
async with self._get_session() as session:
|
|
283
|
+
async for result in self.process_with_concurrency_window(
|
|
284
|
+
items=request_params,
|
|
285
|
+
process_func=lambda params, request_id: self._send_single_request(
|
|
286
|
+
session=session,
|
|
287
|
+
request_id=request_id,
|
|
288
|
+
url=url,
|
|
289
|
+
method=method,
|
|
290
|
+
meta=params.pop('meta', None),
|
|
291
|
+
**params
|
|
292
|
+
),
|
|
293
|
+
concurrency_limit=self._concurrency_limit,
|
|
294
|
+
progress=progress,
|
|
295
|
+
batch_size=batch_size,
|
|
296
|
+
):
|
|
297
|
+
await queue.put(result)
|
|
298
|
+
|
|
299
|
+
await queue.put(None)
|
|
300
|
+
|
|
301
|
+
async def aiter_stream_requests(self,
|
|
302
|
+
request_params: Iterable[Dict[str, Any]],
|
|
303
|
+
url: str,
|
|
304
|
+
method: str = 'POST',
|
|
305
|
+
total_requests: Optional[int] = None,
|
|
306
|
+
show_progress: bool = True,
|
|
307
|
+
batch_size: Optional[int] = None
|
|
308
|
+
):
|
|
309
|
+
queue = Queue()
|
|
310
|
+
task = asyncio.create_task(self._stream_requests(queue,
|
|
311
|
+
request_params=request_params,
|
|
312
|
+
url=url,
|
|
313
|
+
method=method,
|
|
314
|
+
total_requests=total_requests,
|
|
315
|
+
show_progress=show_progress,
|
|
316
|
+
batch_size=batch_size))
|
|
317
|
+
try:
|
|
318
|
+
while True:
|
|
319
|
+
result = await queue.get()
|
|
320
|
+
if result is None:
|
|
321
|
+
break
|
|
322
|
+
yield result
|
|
323
|
+
finally:
|
|
324
|
+
if not task.done():
|
|
325
|
+
task.cancel()
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
async def process_requests(
|
|
329
|
+
self,
|
|
330
|
+
request_params: Iterable[Dict[str, Any]],
|
|
331
|
+
url: str,
|
|
332
|
+
method: str = 'POST',
|
|
333
|
+
total_requests: Optional[int] = None,
|
|
334
|
+
show_progress: bool = True
|
|
335
|
+
) -> Tuple[List[RequestResult], Optional[ProgressTracker]]:
|
|
336
|
+
"""
|
|
337
|
+
处理批量请求
|
|
338
|
+
|
|
339
|
+
Returns:
|
|
340
|
+
Tuple[list[RequestResult], Optional[ProgressTracker]]:
|
|
341
|
+
请求结果列表和进度跟踪器(如果启用了进度显示)
|
|
342
|
+
"""
|
|
343
|
+
progress = None
|
|
344
|
+
if total_requests is None and show_progress:
|
|
345
|
+
request_params, params_for_counting = itertools.tee(request_params)
|
|
346
|
+
total_requests = sum(1 for _ in params_for_counting)
|
|
347
|
+
|
|
348
|
+
if show_progress and total_requests is not None:
|
|
349
|
+
progress = ProgressTracker(
|
|
350
|
+
total_requests,
|
|
351
|
+
concurrency=self._concurrency_limit,
|
|
352
|
+
config=ProgressBarConfig()
|
|
353
|
+
)
|
|
354
|
+
|
|
355
|
+
results = []
|
|
356
|
+
async with self._get_session() as session:
|
|
357
|
+
async for result in self.process_with_concurrency_window(
|
|
358
|
+
items=request_params,
|
|
359
|
+
process_func=lambda params, request_id: self._send_single_request(
|
|
360
|
+
session=session,
|
|
361
|
+
request_id=request_id,
|
|
362
|
+
url=url,
|
|
363
|
+
method=method,
|
|
364
|
+
meta=params.pop('meta', None),
|
|
365
|
+
**params
|
|
366
|
+
),
|
|
367
|
+
concurrency_limit=self._concurrency_limit,
|
|
368
|
+
progress=progress,
|
|
369
|
+
):
|
|
370
|
+
results.extend(result.completed_requests)
|
|
371
|
+
# sort
|
|
372
|
+
results = sorted(results, key=lambda x: x.request_id)
|
|
373
|
+
return results, progress
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
from rich.markdown import Markdown
|
|
4
|
+
from typing import Dict, List, Optional, TYPE_CHECKING
|
|
5
|
+
import time
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
import statistics
|
|
8
|
+
from collections import defaultdict
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from .interface import RequestResult
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ProgressBarStyle(Enum):
|
|
15
|
+
SOLID = ("█", "─", "⚡") # 实心样式
|
|
16
|
+
BLANK = ("▉", " ", "⚡")
|
|
17
|
+
GRADIENT = ("▰", "▱", "⚡") # 渐变样式
|
|
18
|
+
BLOCKS = ("▣", "▢", "⚡") # 方块样式
|
|
19
|
+
ARROW = ("━", "─", "⚡") # 箭头样式
|
|
20
|
+
DOTS = ("⣿", "⣀", "⚡") # 点状样式
|
|
21
|
+
PIPES = ("┃", "┆", "⚡") # 管道样式
|
|
22
|
+
STARS = ("★", "☆", "⚡") # 星星样式
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@dataclass
|
|
26
|
+
class ProgressBarConfig:
|
|
27
|
+
bar_length: int = 30
|
|
28
|
+
show_percentage: bool = True
|
|
29
|
+
show_speed: bool = True
|
|
30
|
+
show_counts: bool = True
|
|
31
|
+
show_time_stats: bool = True
|
|
32
|
+
style: ProgressBarStyle = ProgressBarStyle.BLANK
|
|
33
|
+
use_colors: bool = True
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class ProgressTracker:
|
|
37
|
+
def __init__(
|
|
38
|
+
self,
|
|
39
|
+
total_requests: int,
|
|
40
|
+
concurrency=1,
|
|
41
|
+
config: Optional[ProgressBarConfig] = None,
|
|
42
|
+
):
|
|
43
|
+
self.console = Console()
|
|
44
|
+
|
|
45
|
+
# 统计信息
|
|
46
|
+
self.success_count = 0
|
|
47
|
+
self.error_count = 0
|
|
48
|
+
self.latencies: List[float] = []
|
|
49
|
+
self.errors: Dict[str, int] = defaultdict(int) # 统计不同类型的错误
|
|
50
|
+
|
|
51
|
+
self.total_requests = total_requests
|
|
52
|
+
self.concurrency = concurrency
|
|
53
|
+
self.config = config or ProgressBarConfig()
|
|
54
|
+
self.completed_requests = 0
|
|
55
|
+
self.success_count = 0
|
|
56
|
+
self.error_count = 0
|
|
57
|
+
self.start_time = time.time()
|
|
58
|
+
self.latencies = []
|
|
59
|
+
self.errors = {}
|
|
60
|
+
self.last_speed_update = time.time()
|
|
61
|
+
self.recent_latencies = [] # 用于计算实时速度
|
|
62
|
+
|
|
63
|
+
# ANSI颜色代码
|
|
64
|
+
self.colors = {
|
|
65
|
+
"green": "\033[92m",
|
|
66
|
+
"yellow": "\033[93m",
|
|
67
|
+
"red": "\033[91m",
|
|
68
|
+
"blue": "\033[94m",
|
|
69
|
+
"purple": "\033[95m",
|
|
70
|
+
"cyan": "\033[96m",
|
|
71
|
+
"reset": "\033[0m",
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
def _format_time(self, seconds: float) -> str:
|
|
75
|
+
"""格式化时间显示"""
|
|
76
|
+
if seconds > 3600:
|
|
77
|
+
return f"{seconds / 3600:.1f}h"
|
|
78
|
+
if seconds > 60:
|
|
79
|
+
return f"{seconds / 60:.1f}m"
|
|
80
|
+
return f"{seconds:.1f}s"
|
|
81
|
+
|
|
82
|
+
@staticmethod
|
|
83
|
+
def _format_speed(speed: float) -> str:
|
|
84
|
+
"""格式化速度显示"""
|
|
85
|
+
# if speed >= 1:
|
|
86
|
+
return f"{speed:.1f} req/s"
|
|
87
|
+
# return f'{speed*1000:.0f} req/ms'
|
|
88
|
+
|
|
89
|
+
def _get_colored_text(self, text: str, color: str) -> str:
|
|
90
|
+
"""添加颜色到文本"""
|
|
91
|
+
if self.config.use_colors:
|
|
92
|
+
return f"{self.colors[color]}{text}{self.colors['reset']}"
|
|
93
|
+
return text
|
|
94
|
+
|
|
95
|
+
def _calculate_speed(self) -> float:
|
|
96
|
+
"""计算实际吞吐量(已完成请求数 / 已用时间)"""
|
|
97
|
+
elapsed = time.time() - self.start_time
|
|
98
|
+
if elapsed <= 0:
|
|
99
|
+
return 0
|
|
100
|
+
return self.completed_requests / elapsed
|
|
101
|
+
|
|
102
|
+
def update(self, result: "RequestResult") -> None:
|
|
103
|
+
"""
|
|
104
|
+
更新进度和统计信息
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
result: 请求结果
|
|
108
|
+
"""
|
|
109
|
+
self.completed_requests += 1
|
|
110
|
+
self.latencies.append(result.latency)
|
|
111
|
+
self.recent_latencies.append(result.latency)
|
|
112
|
+
|
|
113
|
+
# 只保留最近的30个请求用于计算速度
|
|
114
|
+
if len(self.recent_latencies) > 30:
|
|
115
|
+
self.recent_latencies.pop(0)
|
|
116
|
+
|
|
117
|
+
if result.status == "success":
|
|
118
|
+
self.success_count += 1
|
|
119
|
+
else:
|
|
120
|
+
self.error_count += 1
|
|
121
|
+
# 安全地获取错误类型,处理 result.data 为 None 的情况
|
|
122
|
+
if result.data and isinstance(result.data, dict):
|
|
123
|
+
error_type = result.data.get("error", "unknown")
|
|
124
|
+
else:
|
|
125
|
+
error_type = "unknown"
|
|
126
|
+
self.errors[error_type] = self.errors.get(error_type, 0) + 1
|
|
127
|
+
|
|
128
|
+
current_time = time.time()
|
|
129
|
+
total_time = current_time - self.start_time
|
|
130
|
+
progress = self.completed_requests / self.total_requests
|
|
131
|
+
|
|
132
|
+
# 计算统计信息
|
|
133
|
+
speed = self._calculate_speed()
|
|
134
|
+
avg_latency = statistics.mean(self.latencies) if self.latencies else 0
|
|
135
|
+
remaining_requests = self.total_requests - self.completed_requests
|
|
136
|
+
estimated_remaining_time = (
|
|
137
|
+
avg_latency * remaining_requests / self.concurrency
|
|
138
|
+
if avg_latency > 0
|
|
139
|
+
else 0
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
# 创建进度条
|
|
143
|
+
style = self.config.style.value
|
|
144
|
+
filled_length = int(self.config.bar_length * progress)
|
|
145
|
+
bar = style[0] * filled_length + style[1] * (
|
|
146
|
+
self.config.bar_length - filled_length
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# 构建输出组件
|
|
150
|
+
components = []
|
|
151
|
+
|
|
152
|
+
# 进度条和百分比
|
|
153
|
+
progress_text = f"[{self._get_colored_text(bar, 'blue')}]"
|
|
154
|
+
if self.config.show_percentage:
|
|
155
|
+
progress_text += (
|
|
156
|
+
f" {self._get_colored_text(f'{progress * 100:.1f}%', 'green')}"
|
|
157
|
+
)
|
|
158
|
+
components.append(progress_text)
|
|
159
|
+
|
|
160
|
+
# 请求计数
|
|
161
|
+
if self.config.show_counts:
|
|
162
|
+
counts = f"({self.completed_requests}/{self.total_requests})"
|
|
163
|
+
components.append(self._get_colored_text(counts, "yellow"))
|
|
164
|
+
|
|
165
|
+
# 速度信息
|
|
166
|
+
if self.config.show_speed:
|
|
167
|
+
speed_text = f"{style[2]} {self._format_speed(speed)}"
|
|
168
|
+
components.append(self._get_colored_text(speed_text, "cyan"))
|
|
169
|
+
|
|
170
|
+
# 时间统计
|
|
171
|
+
if self.config.show_time_stats:
|
|
172
|
+
time_stats = (
|
|
173
|
+
f"avg: {self._format_time(avg_latency)} "
|
|
174
|
+
f"total: {self._format_time(total_time)} "
|
|
175
|
+
f"eta: {self._format_time(estimated_remaining_time)}"
|
|
176
|
+
)
|
|
177
|
+
components.append(self._get_colored_text(time_stats, "purple"))
|
|
178
|
+
|
|
179
|
+
# 打印进度 - 修复Windows编码问题
|
|
180
|
+
try:
|
|
181
|
+
print("\r" + " ".join(components), end="", flush=True)
|
|
182
|
+
except UnicodeEncodeError:
|
|
183
|
+
# Windows GBK编码兼容处理
|
|
184
|
+
safe_components = []
|
|
185
|
+
for comp in components:
|
|
186
|
+
# 替换有问题的Unicode字符
|
|
187
|
+
safe_comp = comp.replace("⚡", "*").replace("█", "#").replace("─", "-")
|
|
188
|
+
safe_comp = (
|
|
189
|
+
safe_comp.replace("▉", "|").replace("▰", "=").replace("▱", "-")
|
|
190
|
+
)
|
|
191
|
+
safe_comp = (
|
|
192
|
+
safe_comp.replace("▣", "[").replace("▢", "]").replace("━", "=")
|
|
193
|
+
)
|
|
194
|
+
safe_comp = (
|
|
195
|
+
safe_comp.replace("┃", "|")
|
|
196
|
+
.replace("┆", ":")
|
|
197
|
+
.replace("★", "*")
|
|
198
|
+
.replace("☆", "+")
|
|
199
|
+
)
|
|
200
|
+
safe_comp = safe_comp.replace("⣿", "#").replace("⣀", ".")
|
|
201
|
+
safe_components.append(safe_comp)
|
|
202
|
+
print("\r" + " ".join(safe_components), end="", flush=True)
|
|
203
|
+
|
|
204
|
+
def summary(self, show_p999=False, print_to_console=True) -> str:
|
|
205
|
+
"""打印请求汇总信息"""
|
|
206
|
+
total_time = time.time() - self.start_time
|
|
207
|
+
avg_latency = sum(self.latencies) / len(self.latencies) if self.latencies else 0
|
|
208
|
+
throughput = self.success_count / total_time if total_time > 0 else 0
|
|
209
|
+
|
|
210
|
+
# 计算延迟分位数
|
|
211
|
+
sorted_latencies = sorted(self.latencies)
|
|
212
|
+
p50 = p95 = p99 = 0
|
|
213
|
+
if sorted_latencies:
|
|
214
|
+
p50 = sorted_latencies[int(len(sorted_latencies) * 0.5)]
|
|
215
|
+
p95 = sorted_latencies[int(len(sorted_latencies) * 0.95)]
|
|
216
|
+
p99 = sorted_latencies[int(len(sorted_latencies) * 0.99)]
|
|
217
|
+
p995 = sorted_latencies[int(len(sorted_latencies) * 0.995)]
|
|
218
|
+
p999 = sorted_latencies[int(len(sorted_latencies) * 0.999)]
|
|
219
|
+
|
|
220
|
+
p99_str = f"> - P99 延迟: {p99:.2f} 秒"
|
|
221
|
+
p999_str = f"""> - P99 延迟: {p99:.2f} 秒
|
|
222
|
+
> - P995 延迟: {p995:.2f} 秒
|
|
223
|
+
> - P999 延迟: {p999:.2f} 秒"""
|
|
224
|
+
p99_or_p999_str = p999_str if show_p999 else p99_str
|
|
225
|
+
|
|
226
|
+
summary = f"""
|
|
227
|
+
请求统计
|
|
228
|
+
|
|
229
|
+
| 总体情况
|
|
230
|
+
| - 总请求数: {self.total_requests}
|
|
231
|
+
| - 成功请求数: {self.success_count}
|
|
232
|
+
| - 失败请求数: {self.error_count}
|
|
233
|
+
| - 成功率: {(self.success_count / self.total_requests * 100):.2f}%
|
|
234
|
+
|
|
235
|
+
| 性能指标
|
|
236
|
+
| - 平均延迟: {avg_latency:.2f} 秒
|
|
237
|
+
| - P50 延迟: {p50:.2f} 秒
|
|
238
|
+
| - P95 延迟: {p95:.2f} 秒
|
|
239
|
+
| - P99 延迟: {p99:.2f} 秒
|
|
240
|
+
| - 吞吐量: {throughput:.2f} 请求/秒
|
|
241
|
+
| - 总运行时间: {total_time:.2f} 秒
|
|
242
|
+
|
|
243
|
+
"""
|
|
244
|
+
# 如果有错误,添加错误统计
|
|
245
|
+
if self.errors:
|
|
246
|
+
summary += "| 错误分布 \n"
|
|
247
|
+
for error_type, count in self.errors.items():
|
|
248
|
+
percentage = count / self.error_count * 100
|
|
249
|
+
summary += f"| - {error_type}: {count} ({percentage:.1f}%) \n"
|
|
250
|
+
|
|
251
|
+
summary += "-" * 76
|
|
252
|
+
if print_to_console:
|
|
253
|
+
print() # 打印空行
|
|
254
|
+
try:
|
|
255
|
+
# 尝试使用Rich输出,如果失败则使用普通print
|
|
256
|
+
self.console.print(summary)
|
|
257
|
+
except UnicodeEncodeError:
|
|
258
|
+
# 在Windows GBK环境下,如果出现编码错误,使用普通print
|
|
259
|
+
print(summary)
|
|
260
|
+
return summary
|
|
261
|
+
|
|
262
|
+
|
|
263
|
+
if __name__ == "__main__":
|
|
264
|
+
from .interface import RequestResult
|
|
265
|
+
|
|
266
|
+
config = ProgressBarConfig()
|
|
267
|
+
tracker = ProgressTracker(100, 1, config)
|
|
268
|
+
for i in range(100):
|
|
269
|
+
time.sleep(0.1)
|
|
270
|
+
tracker.update(
|
|
271
|
+
result=RequestResult(
|
|
272
|
+
request_id=i,
|
|
273
|
+
data=None,
|
|
274
|
+
status="success",
|
|
275
|
+
latency=0.1,
|
|
276
|
+
)
|
|
277
|
+
)
|