x-server-utils 0.1.0__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.
- x_server_utils-0.1.0/MANIFEST.in +1 -0
- x_server_utils-0.1.0/PKG-INFO +66 -0
- x_server_utils-0.1.0/README.md +39 -0
- x_server_utils-0.1.0/setup.cfg +4 -0
- x_server_utils-0.1.0/setup.py +30 -0
- x_server_utils-0.1.0/x_server_utils/__init__.py +3 -0
- x_server_utils-0.1.0/x_server_utils/core.py +317 -0
- x_server_utils-0.1.0/x_server_utils/txt/CHANGELOG.md +4 -0
- x_server_utils-0.1.0/x_server_utils.egg-info/PKG-INFO +66 -0
- x_server_utils-0.1.0/x_server_utils.egg-info/SOURCES.txt +11 -0
- x_server_utils-0.1.0/x_server_utils.egg-info/dependency_links.txt +1 -0
- x_server_utils-0.1.0/x_server_utils.egg-info/requires.txt +4 -0
- x_server_utils-0.1.0/x_server_utils.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
recursive-include x_server_utils/txt *.md
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: x-server-utils
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A collection of FastAPI Server Utilities and Stress Tester
|
|
5
|
+
Home-page: https://github.com/nodame2233/x-server-utils
|
|
6
|
+
Author: Xuan
|
|
7
|
+
Author-email: 786625468@qq.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.11
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: fastapi
|
|
15
|
+
Requires-Dist: uvicorn
|
|
16
|
+
Requires-Dist: requests
|
|
17
|
+
Requires-Dist: loguru
|
|
18
|
+
Dynamic: author
|
|
19
|
+
Dynamic: author-email
|
|
20
|
+
Dynamic: classifier
|
|
21
|
+
Dynamic: description
|
|
22
|
+
Dynamic: description-content-type
|
|
23
|
+
Dynamic: home-page
|
|
24
|
+
Dynamic: requires-dist
|
|
25
|
+
Dynamic: requires-python
|
|
26
|
+
Dynamic: summary
|
|
27
|
+
|
|
28
|
+
# x-server-utils
|
|
29
|
+
|
|
30
|
+
FastAPI 服务器工具集与压力测试工具
|
|
31
|
+
|
|
32
|
+
## 安装
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install x-server-utils
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 快速开始
|
|
39
|
+
|
|
40
|
+
python
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
from x_server_utils import core
|
|
44
|
+
|
|
45
|
+
# 使用示例(根据你的实际功能调整)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
## 功能
|
|
51
|
+
|
|
52
|
+
- FastAPI 服务器工具函数
|
|
53
|
+
- HTTP 压力测试
|
|
54
|
+
- 日志管理(基于 loguru)
|
|
55
|
+
|
|
56
|
+
## 依赖
|
|
57
|
+
|
|
58
|
+
- Python >= 3.11
|
|
59
|
+
- fastapi
|
|
60
|
+
- uvicorn
|
|
61
|
+
- requests
|
|
62
|
+
- loguru
|
|
63
|
+
|
|
64
|
+
## 作者
|
|
65
|
+
|
|
66
|
+
Xuan - 786625468@qq.com
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# x-server-utils
|
|
2
|
+
|
|
3
|
+
FastAPI 服务器工具集与压力测试工具
|
|
4
|
+
|
|
5
|
+
## 安装
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install x-server-utils
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## 快速开始
|
|
12
|
+
|
|
13
|
+
python
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
from x_server_utils import core
|
|
17
|
+
|
|
18
|
+
# 使用示例(根据你的实际功能调整)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
## 功能
|
|
24
|
+
|
|
25
|
+
- FastAPI 服务器工具函数
|
|
26
|
+
- HTTP 压力测试
|
|
27
|
+
- 日志管理(基于 loguru)
|
|
28
|
+
|
|
29
|
+
## 依赖
|
|
30
|
+
|
|
31
|
+
- Python >= 3.11
|
|
32
|
+
- fastapi
|
|
33
|
+
- uvicorn
|
|
34
|
+
- requests
|
|
35
|
+
- loguru
|
|
36
|
+
|
|
37
|
+
## 作者
|
|
38
|
+
|
|
39
|
+
Xuan - 786625468@qq.com
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
|
|
3
|
+
with open("README.md", "r", encoding="utf-8") as fh:
|
|
4
|
+
long_description = fh.read()
|
|
5
|
+
|
|
6
|
+
setup(
|
|
7
|
+
name="x-server-utils", # PyPI 上的包名
|
|
8
|
+
version="0.1.0",
|
|
9
|
+
author="Xuan",
|
|
10
|
+
author_email="786625468@qq.com",
|
|
11
|
+
description="A collection of FastAPI Server Utilities and Stress Tester",
|
|
12
|
+
long_description=long_description,
|
|
13
|
+
long_description_content_type="text/markdown",
|
|
14
|
+
url="https://github.com/nodame2233/x-server-utils",
|
|
15
|
+
packages=find_packages(),
|
|
16
|
+
include_package_data=True,
|
|
17
|
+
install_requires=[
|
|
18
|
+
"fastapi",
|
|
19
|
+
"uvicorn",
|
|
20
|
+
"requests",
|
|
21
|
+
"loguru",
|
|
22
|
+
],
|
|
23
|
+
classifiers=[
|
|
24
|
+
"Programming Language :: Python :: 3",
|
|
25
|
+
"Programming Language :: Python :: 3.11",
|
|
26
|
+
"License :: OSI Approved :: MIT License", # 或你选择的许可证
|
|
27
|
+
"Operating System :: OS Independent",
|
|
28
|
+
],
|
|
29
|
+
python_requires=">=3.11",
|
|
30
|
+
)
|
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
# -*- coding: utf-8 -*-
|
|
2
|
+
import re
|
|
3
|
+
import os
|
|
4
|
+
import traceback
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from fastapi import FastAPI, Request as FastAPIRequest
|
|
8
|
+
from fastapi.exceptions import RequestValidationError
|
|
9
|
+
from starlette.responses import JSONResponse
|
|
10
|
+
import __main__ # noqa
|
|
11
|
+
import uvicorn
|
|
12
|
+
import socket
|
|
13
|
+
import requests
|
|
14
|
+
import time
|
|
15
|
+
import argparse
|
|
16
|
+
import concurrent.futures
|
|
17
|
+
from loguru import logger
|
|
18
|
+
BASE_DIR = os.path.dirname(os.path.abspath(__file__))
|
|
19
|
+
CHANGELOG_PATH = os.path.join(BASE_DIR, "txt", "CHANGELOG.md")
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class ResponseCode:
|
|
23
|
+
"""响应状态码配置"""
|
|
24
|
+
SUCCESS = (0, "success")
|
|
25
|
+
ERROR = (300, "error")
|
|
26
|
+
FAIL = (400, "fail")
|
|
27
|
+
UNAUTHORIZED = (401, "unauthorized")
|
|
28
|
+
NOT_FOUND = (404, "not found")
|
|
29
|
+
EXCEED_TIME = (408, "request timeout")
|
|
30
|
+
INNER_ERROR = (500, "internal server error")
|
|
31
|
+
ROBOT_VERIFY = (1001, "robot verify required")
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class ServerUtil(object):
|
|
35
|
+
@staticmethod
|
|
36
|
+
def _resolve_app_target(explicit_app: str | None = None):
|
|
37
|
+
"""
|
|
38
|
+
Resolve ASGI app import target and app_dir for uvicorn.
|
|
39
|
+
Returns:
|
|
40
|
+
tuple[str, str | None]: ("module:app", app_dir)
|
|
41
|
+
"""
|
|
42
|
+
if explicit_app:
|
|
43
|
+
explicit_app = explicit_app.strip()
|
|
44
|
+
if ":" not in explicit_app:
|
|
45
|
+
raise ValueError("--app 参数格式错误,需为 'module:app'")
|
|
46
|
+
return explicit_app, None
|
|
47
|
+
|
|
48
|
+
app_dir = None
|
|
49
|
+
module_name = None
|
|
50
|
+
|
|
51
|
+
main_spec = getattr(__main__, "__spec__", None)
|
|
52
|
+
if main_spec and getattr(main_spec, "name", None) and main_spec.name != "__main__":
|
|
53
|
+
module_name = main_spec.name
|
|
54
|
+
|
|
55
|
+
main_file = getattr(__main__, "__file__", None)
|
|
56
|
+
if main_file:
|
|
57
|
+
main_path = Path(main_file).resolve()
|
|
58
|
+
app_dir = str(main_path.parent)
|
|
59
|
+
if not module_name:
|
|
60
|
+
module_name = main_path.stem
|
|
61
|
+
|
|
62
|
+
if not module_name:
|
|
63
|
+
raise RuntimeError("无法自动解析启动模块,请通过 --app 显式指定,例如 --app chemparse_server:app")
|
|
64
|
+
|
|
65
|
+
return f"{module_name}:app", app_dir
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def _normalize_workers(workers: int):
|
|
69
|
+
"""Ensure workers is always >= 1."""
|
|
70
|
+
if workers is None:
|
|
71
|
+
return 1
|
|
72
|
+
return max(1, workers)
|
|
73
|
+
|
|
74
|
+
@staticmethod
|
|
75
|
+
def _is_linux_container():
|
|
76
|
+
"""Best-effort check for Linux container environments."""
|
|
77
|
+
if os.name == "nt":
|
|
78
|
+
return False
|
|
79
|
+
if os.path.exists("/.dockerenv"):
|
|
80
|
+
return True
|
|
81
|
+
cgroup_path = "/proc/1/cgroup"
|
|
82
|
+
if os.path.exists(cgroup_path):
|
|
83
|
+
try:
|
|
84
|
+
with open(cgroup_path, "r", encoding="utf-8") as f:
|
|
85
|
+
cgroup_data = f.read()
|
|
86
|
+
if "docker" in cgroup_data or "kubepods" in cgroup_data or "containerd" in cgroup_data:
|
|
87
|
+
return True
|
|
88
|
+
except Exception: # noqa
|
|
89
|
+
pass
|
|
90
|
+
return False
|
|
91
|
+
|
|
92
|
+
@staticmethod
|
|
93
|
+
def get_server_description():
|
|
94
|
+
"""
|
|
95
|
+
读取描述文件和更新日志,并提取最新版本号
|
|
96
|
+
:return: (changelog_content: str, latest_version: str)
|
|
97
|
+
"""
|
|
98
|
+
changelog_content = ""
|
|
99
|
+
latest_version = "未知版本" # 默认值,防止没匹配到时报错
|
|
100
|
+
try:
|
|
101
|
+
if os.path.exists(CHANGELOG_PATH):
|
|
102
|
+
with open(CHANGELOG_PATH, "r", encoding="utf-8") as f:
|
|
103
|
+
changelog_content = f.read()
|
|
104
|
+
match = re.search(r"#{2,4}\s*\[([^]]+)]", changelog_content)
|
|
105
|
+
if match:
|
|
106
|
+
latest_version = match.group(1) # 提取括号里面的内容,例如 1.4.0
|
|
107
|
+
except Exception as e:
|
|
108
|
+
logger.warning(f"读取更新日志失败: {e}")
|
|
109
|
+
return changelog_content, latest_version
|
|
110
|
+
|
|
111
|
+
@staticmethod
|
|
112
|
+
def run_server(project_name: str, default_port: int = 8000, require_inner_url: bool = False):
|
|
113
|
+
"""
|
|
114
|
+
Uvicorn 启动入口,支持跨平台与容器场景。
|
|
115
|
+
:param project_name: 项目名称
|
|
116
|
+
:param default_port: 默认端口
|
|
117
|
+
:param require_inner_url: 是否强依赖内部接口地址
|
|
118
|
+
"""
|
|
119
|
+
parser = argparse.ArgumentParser(description=f"{project_name} API Service")
|
|
120
|
+
parser.add_argument("-H", "--host", type=str, default="0.0.0.0", help="绑定地址 (默认: 0.0.0.0)")
|
|
121
|
+
parser.add_argument("-p", "--port", type=int, default=default_port, help=f"启动端口 (默认: {default_port})")
|
|
122
|
+
parser.add_argument("-w", "--workers", type=int, default=1, help="工作进程数 (默认: 1)")
|
|
123
|
+
parser.add_argument("-a", "--app", type=str, default=None, help="ASGI 入口,例如 chemparse_server:app")
|
|
124
|
+
parser.add_argument("-l", "--log-level", type=str, default="info", help="日志级别 (默认: info)")
|
|
125
|
+
parser.add_argument(
|
|
126
|
+
"--limit-max-requests",
|
|
127
|
+
type=int,
|
|
128
|
+
default=0,
|
|
129
|
+
help="每个 worker 最多处理请求数,达到后自动重启 (0 表示不限制)"
|
|
130
|
+
)
|
|
131
|
+
parser.add_argument(
|
|
132
|
+
"--timeout-worker-healthcheck",
|
|
133
|
+
type=int,
|
|
134
|
+
default=10,
|
|
135
|
+
help="worker 健康检查超时秒数 (默认: 10)"
|
|
136
|
+
)
|
|
137
|
+
parser.add_argument(
|
|
138
|
+
"-i",
|
|
139
|
+
"--inner_url",
|
|
140
|
+
type=str,
|
|
141
|
+
default=None,
|
|
142
|
+
required=require_inner_url,
|
|
143
|
+
help="内部接口地址" + (" (必填)" if require_inner_url else " (默认: None)")
|
|
144
|
+
)
|
|
145
|
+
parser.add_argument("-U", "--username", type=str, default=None, help="数据库访问账号")
|
|
146
|
+
parser.add_argument("-P", "--password", type=str, default=None, help="数据库访问密码")
|
|
147
|
+
|
|
148
|
+
args = parser.parse_args()
|
|
149
|
+
|
|
150
|
+
host = args.host
|
|
151
|
+
port = args.port
|
|
152
|
+
workers = ServerUtil._normalize_workers(args.workers)
|
|
153
|
+
if args.inner_url:
|
|
154
|
+
os.environ["SERVICE_INNER_URL"] = args.inner_url
|
|
155
|
+
if args.username:
|
|
156
|
+
os.environ["DB_USERNAME"] = args.username
|
|
157
|
+
if args.password:
|
|
158
|
+
os.environ["DB_PASSWORD"] = args.password
|
|
159
|
+
|
|
160
|
+
if workers != args.workers:
|
|
161
|
+
logger.warning(f"workers 参数非法({args.workers}),已自动调整为 {workers}")
|
|
162
|
+
|
|
163
|
+
if workers > 1 and ServerUtil._is_linux_container():
|
|
164
|
+
cpu_count = os.cpu_count() or 1
|
|
165
|
+
recommended_workers = max(1, min(cpu_count, 4))
|
|
166
|
+
if workers > recommended_workers:
|
|
167
|
+
logger.warning(
|
|
168
|
+
f"检测到 Linux 容器环境,当前 workers={workers} 偏高,建议 <= {recommended_workers},"
|
|
169
|
+
f"否则可能出现 OOM 或 'Child process died'。"
|
|
170
|
+
)
|
|
171
|
+
|
|
172
|
+
try:
|
|
173
|
+
local_ip = socket.gethostbyname(socket.gethostname())
|
|
174
|
+
except Exception: # noqa
|
|
175
|
+
local_ip = "127.0.0.1"
|
|
176
|
+
|
|
177
|
+
app_target, app_dir = ServerUtil._resolve_app_target(args.app)
|
|
178
|
+
if app_dir and app_dir not in sys.path:
|
|
179
|
+
sys.path.insert(0, app_dir)
|
|
180
|
+
|
|
181
|
+
logger.info(
|
|
182
|
+
f"\n项目名称: {project_name}\n"
|
|
183
|
+
f"局域网访问: http://{local_ip}:{port}\n"
|
|
184
|
+
f"Swagger文档: http://{local_ip}:{port}/docs\n"
|
|
185
|
+
f"配置参数: host={host}, workers={workers}, app={app_target}, app_dir={app_dir}"
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
uvicorn.run(
|
|
189
|
+
app_target,
|
|
190
|
+
host=host,
|
|
191
|
+
port=port,
|
|
192
|
+
workers=workers,
|
|
193
|
+
app_dir=app_dir,
|
|
194
|
+
log_level=args.log_level,
|
|
195
|
+
timeout_worker_healthcheck=args.timeout_worker_healthcheck,
|
|
196
|
+
limit_max_requests=args.limit_max_requests if args.limit_max_requests > 0 else None,
|
|
197
|
+
reload=False,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
@staticmethod
|
|
201
|
+
async def unified_exception_handler(request: FastAPIRequest, exc: Exception):
|
|
202
|
+
"""
|
|
203
|
+
具体的异常处理逻辑
|
|
204
|
+
"""
|
|
205
|
+
if isinstance(exc, RequestValidationError):
|
|
206
|
+
logger.error(f"【参数校验拦截】 URL: {request.url} \n{str(exc)}")
|
|
207
|
+
else:
|
|
208
|
+
logger.error(f"【全局代码异常拦截】 URL: {request.url} \n{traceback.format_exc()}")
|
|
209
|
+
|
|
210
|
+
status = ResponseCode.INNER_ERROR
|
|
211
|
+
return JSONResponse(
|
|
212
|
+
status_code=500,
|
|
213
|
+
content={
|
|
214
|
+
'code': status[0],
|
|
215
|
+
'data': [],
|
|
216
|
+
'message': status[1]
|
|
217
|
+
}
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
@staticmethod
|
|
221
|
+
def register_global_exceptions(app: FastAPI):
|
|
222
|
+
"""
|
|
223
|
+
暴露给外部的注册函数:将拦截器绑定到传入的 FastAPI 实例上
|
|
224
|
+
"""
|
|
225
|
+
# 相当于 @app.exception_handler(RequestValidationError)
|
|
226
|
+
app.add_exception_handler(RequestValidationError, ServerUtil.unified_exception_handler)
|
|
227
|
+
# 相当于 @app.exception_handler(Exception)
|
|
228
|
+
app.add_exception_handler(Exception, ServerUtil.unified_exception_handler)
|
|
229
|
+
|
|
230
|
+
@staticmethod
|
|
231
|
+
def register_global_middlewares(app: FastAPI):
|
|
232
|
+
"""
|
|
233
|
+
注册全局中间件(如:请求/响应耗时与日志追踪)
|
|
234
|
+
"""
|
|
235
|
+
|
|
236
|
+
@app.middleware("http")
|
|
237
|
+
async def log_request_response(request: FastAPIRequest, call_next):
|
|
238
|
+
client_ip = request.client.host if request.client else "Unknown"
|
|
239
|
+
request_id = int(time.time() * 1000) # 简单的请求链路ID,方便在并发时匹配日志
|
|
240
|
+
|
|
241
|
+
logger.info(
|
|
242
|
+
f"[Req:{request_id}] 收到请求 | 来源IP: {client_ip} | 路径: {request.method} {request.url.path}")
|
|
243
|
+
|
|
244
|
+
start_time = time.time()
|
|
245
|
+
try:
|
|
246
|
+
# 放行请求给后续路由
|
|
247
|
+
response = await call_next(request)
|
|
248
|
+
|
|
249
|
+
process_time = time.time() - start_time
|
|
250
|
+
logger.info(f"[Req:{request_id}] 发送响应 | 状态码: {response.status_code} | 耗时: {process_time:.3f}s")
|
|
251
|
+
return response
|
|
252
|
+
|
|
253
|
+
except Exception as e:
|
|
254
|
+
# 注意:业务抛出的异常其实会被 register_global_exceptions 提前捕获并转为 500 状态码
|
|
255
|
+
# 只有发生框架级/底层异常时,才会走到这里
|
|
256
|
+
process_time = time.time() - start_time
|
|
257
|
+
logger.error(f"[Req:{request_id}] 响应异常 | 耗时: {process_time:.3f}s | 异常信息: {str(e)}")
|
|
258
|
+
raise e
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
class StressTester(object):
|
|
262
|
+
@staticmethod
|
|
263
|
+
def send_request(url, img_data):
|
|
264
|
+
"""发送单次请求并统计时间"""
|
|
265
|
+
start_time = time.time()
|
|
266
|
+
try:
|
|
267
|
+
payload = {'image_base64': img_data}
|
|
268
|
+
response = requests.post(url, json=payload, timeout=40)
|
|
269
|
+
duration = time.time() - start_time
|
|
270
|
+
|
|
271
|
+
if response.status_code == 200:
|
|
272
|
+
return True, duration
|
|
273
|
+
else:
|
|
274
|
+
return False, duration
|
|
275
|
+
except Exception as e:
|
|
276
|
+
duration = time.time() - start_time
|
|
277
|
+
logger.error(f"请求异常: {e}")
|
|
278
|
+
return False, duration
|
|
279
|
+
|
|
280
|
+
@staticmethod
|
|
281
|
+
def run_stress_test(img_data, url, workers, total_requests):
|
|
282
|
+
# 准备图片数据
|
|
283
|
+
logger.info(f"开始压测: URL={url}, 并发数={workers}, 总请求数={total_requests}")
|
|
284
|
+
results = []
|
|
285
|
+
start_wall_time = time.time()
|
|
286
|
+
|
|
287
|
+
# 使用线程池模拟并发
|
|
288
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=workers) as executor:
|
|
289
|
+
# 提交所有任务
|
|
290
|
+
futures = [executor.submit(StressTester.send_request, url, img_data) for _ in range(total_requests)]
|
|
291
|
+
for future in concurrent.futures.as_completed(futures):
|
|
292
|
+
results.append(future.result())
|
|
293
|
+
|
|
294
|
+
end_wall_time = time.time()
|
|
295
|
+
total_wall_time = end_wall_time - start_wall_time
|
|
296
|
+
|
|
297
|
+
# 统计数据
|
|
298
|
+
success_count = sum(1 for r in results if r[0])
|
|
299
|
+
fail_count = total_requests - success_count
|
|
300
|
+
durations = [r[1] for r in results]
|
|
301
|
+
|
|
302
|
+
avg_time = sum(durations) / len(durations) if durations else 0
|
|
303
|
+
qps = total_requests / total_wall_time
|
|
304
|
+
|
|
305
|
+
print("\n" + "=" * 50)
|
|
306
|
+
print("压测结果报告")
|
|
307
|
+
print("=" * 50)
|
|
308
|
+
print(f"并发数 (Workers): {workers}")
|
|
309
|
+
print(f"总请求数: {total_requests}")
|
|
310
|
+
print(f"成功次数: {success_count}")
|
|
311
|
+
print(f"失败次数: {fail_count}")
|
|
312
|
+
print(f"总耗时: {total_wall_time:.2f} 秒")
|
|
313
|
+
print(f"每秒请求数 (QPS): {qps:.2f}")
|
|
314
|
+
print(f"平均响应时间: {avg_time * 1000:.2f} 毫秒")
|
|
315
|
+
print(f"最快响应时间: {min(durations) * 1000:.2f} 毫秒")
|
|
316
|
+
print(f"最慢响应时间: {max(durations) * 1000:.2f} 毫秒")
|
|
317
|
+
print("=" * 50)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: x-server-utils
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A collection of FastAPI Server Utilities and Stress Tester
|
|
5
|
+
Home-page: https://github.com/nodame2233/x-server-utils
|
|
6
|
+
Author: Xuan
|
|
7
|
+
Author-email: 786625468@qq.com
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.11
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: fastapi
|
|
15
|
+
Requires-Dist: uvicorn
|
|
16
|
+
Requires-Dist: requests
|
|
17
|
+
Requires-Dist: loguru
|
|
18
|
+
Dynamic: author
|
|
19
|
+
Dynamic: author-email
|
|
20
|
+
Dynamic: classifier
|
|
21
|
+
Dynamic: description
|
|
22
|
+
Dynamic: description-content-type
|
|
23
|
+
Dynamic: home-page
|
|
24
|
+
Dynamic: requires-dist
|
|
25
|
+
Dynamic: requires-python
|
|
26
|
+
Dynamic: summary
|
|
27
|
+
|
|
28
|
+
# x-server-utils
|
|
29
|
+
|
|
30
|
+
FastAPI 服务器工具集与压力测试工具
|
|
31
|
+
|
|
32
|
+
## 安装
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install x-server-utils
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
## 快速开始
|
|
39
|
+
|
|
40
|
+
python
|
|
41
|
+
|
|
42
|
+
```
|
|
43
|
+
from x_server_utils import core
|
|
44
|
+
|
|
45
|
+
# 使用示例(根据你的实际功能调整)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
## 功能
|
|
51
|
+
|
|
52
|
+
- FastAPI 服务器工具函数
|
|
53
|
+
- HTTP 压力测试
|
|
54
|
+
- 日志管理(基于 loguru)
|
|
55
|
+
|
|
56
|
+
## 依赖
|
|
57
|
+
|
|
58
|
+
- Python >= 3.11
|
|
59
|
+
- fastapi
|
|
60
|
+
- uvicorn
|
|
61
|
+
- requests
|
|
62
|
+
- loguru
|
|
63
|
+
|
|
64
|
+
## 作者
|
|
65
|
+
|
|
66
|
+
Xuan - 786625468@qq.com
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
MANIFEST.in
|
|
2
|
+
README.md
|
|
3
|
+
setup.py
|
|
4
|
+
x_server_utils/__init__.py
|
|
5
|
+
x_server_utils/core.py
|
|
6
|
+
x_server_utils.egg-info/PKG-INFO
|
|
7
|
+
x_server_utils.egg-info/SOURCES.txt
|
|
8
|
+
x_server_utils.egg-info/dependency_links.txt
|
|
9
|
+
x_server_utils.egg-info/requires.txt
|
|
10
|
+
x_server_utils.egg-info/top_level.txt
|
|
11
|
+
x_server_utils/txt/CHANGELOG.md
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
x_server_utils
|