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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,3 @@
1
+ from .core import ResponseCode, ServerUtil, StressTester
2
+
3
+ __all__ = ["ResponseCode", "ServerUtil", "StressTester"]
@@ -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,4 @@
1
+ ### 更新日志 (Changelog)
2
+
3
+ #### [0.1.0]
4
+ - **优化**: 重构ServerUtil中的run_server方法,兼容linux环境启动
@@ -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,4 @@
1
+ fastapi
2
+ uvicorn
3
+ requests
4
+ loguru
@@ -0,0 +1 @@
1
+ x_server_utils