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.
Files changed (39) hide show
  1. flexllm/__init__.py +224 -0
  2. flexllm/__main__.py +1096 -0
  3. flexllm/async_api/__init__.py +9 -0
  4. flexllm/async_api/concurrent_call.py +100 -0
  5. flexllm/async_api/concurrent_executor.py +1036 -0
  6. flexllm/async_api/core.py +373 -0
  7. flexllm/async_api/interface.py +12 -0
  8. flexllm/async_api/progress.py +277 -0
  9. flexllm/base_client.py +988 -0
  10. flexllm/batch_tools/__init__.py +16 -0
  11. flexllm/batch_tools/folder_processor.py +317 -0
  12. flexllm/batch_tools/table_processor.py +363 -0
  13. flexllm/cache/__init__.py +10 -0
  14. flexllm/cache/response_cache.py +293 -0
  15. flexllm/chain_of_thought_client.py +1120 -0
  16. flexllm/claudeclient.py +402 -0
  17. flexllm/client_pool.py +698 -0
  18. flexllm/geminiclient.py +563 -0
  19. flexllm/llm_client.py +523 -0
  20. flexllm/llm_parser.py +60 -0
  21. flexllm/mllm_client.py +559 -0
  22. flexllm/msg_processors/__init__.py +174 -0
  23. flexllm/msg_processors/image_processor.py +729 -0
  24. flexllm/msg_processors/image_processor_helper.py +485 -0
  25. flexllm/msg_processors/messages_processor.py +341 -0
  26. flexllm/msg_processors/unified_processor.py +1404 -0
  27. flexllm/openaiclient.py +256 -0
  28. flexllm/pricing/__init__.py +104 -0
  29. flexllm/pricing/data.json +1201 -0
  30. flexllm/pricing/updater.py +223 -0
  31. flexllm/provider_router.py +213 -0
  32. flexllm/token_counter.py +270 -0
  33. flexllm/utils/__init__.py +1 -0
  34. flexllm/utils/core.py +41 -0
  35. flexllm-0.3.3.dist-info/METADATA +573 -0
  36. flexllm-0.3.3.dist-info/RECORD +39 -0
  37. flexllm-0.3.3.dist-info/WHEEL +4 -0
  38. flexllm-0.3.3.dist-info/entry_points.txt +3 -0
  39. 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,12 @@
1
+ from dataclasses import dataclass
2
+ from typing import Any, Optional
3
+
4
+
5
+ @dataclass
6
+ class RequestResult:
7
+ """请求结果的数据类"""
8
+ request_id: int
9
+ data: Any
10
+ status: str
11
+ latency: float
12
+ meta: dict = None
@@ -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
+ )