infomankit 0.3.23__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.
- infoman/__init__.py +1 -0
- infoman/cli/README.md +378 -0
- infoman/cli/__init__.py +7 -0
- infoman/cli/commands/__init__.py +3 -0
- infoman/cli/commands/init.py +312 -0
- infoman/cli/scaffold.py +634 -0
- infoman/cli/templates/Makefile.template +132 -0
- infoman/cli/templates/app/__init__.py.template +3 -0
- infoman/cli/templates/app/app.py.template +4 -0
- infoman/cli/templates/app/models_base.py.template +18 -0
- infoman/cli/templates/app/models_entity_init.py.template +11 -0
- infoman/cli/templates/app/models_schemas_init.py.template +11 -0
- infoman/cli/templates/app/repository_init.py.template +11 -0
- infoman/cli/templates/app/routers_init.py.template +15 -0
- infoman/cli/templates/app/services_init.py.template +11 -0
- infoman/cli/templates/app/static_index.html.template +39 -0
- infoman/cli/templates/app/static_main.js.template +31 -0
- infoman/cli/templates/app/static_style.css.template +111 -0
- infoman/cli/templates/app/utils_init.py.template +11 -0
- infoman/cli/templates/config/.env.dev.template +43 -0
- infoman/cli/templates/config/.env.prod.template +43 -0
- infoman/cli/templates/config/README.md.template +28 -0
- infoman/cli/templates/docker/.dockerignore.template +60 -0
- infoman/cli/templates/docker/Dockerfile.template +47 -0
- infoman/cli/templates/docker/README.md.template +240 -0
- infoman/cli/templates/docker/docker-compose.yml.template +81 -0
- infoman/cli/templates/docker/mysql_custom.cnf.template +42 -0
- infoman/cli/templates/docker/mysql_init.sql.template +15 -0
- infoman/cli/templates/project/.env.example.template +1 -0
- infoman/cli/templates/project/.gitignore.template +60 -0
- infoman/cli/templates/project/Makefile.template +38 -0
- infoman/cli/templates/project/README.md.template +137 -0
- infoman/cli/templates/project/deploy.sh.template +97 -0
- infoman/cli/templates/project/main.py.template +10 -0
- infoman/cli/templates/project/manage.sh.template +97 -0
- infoman/cli/templates/project/pyproject.toml.template +47 -0
- infoman/cli/templates/project/service.sh.template +203 -0
- infoman/config/__init__.py +25 -0
- infoman/config/base.py +67 -0
- infoman/config/db_cache.py +237 -0
- infoman/config/db_relation.py +181 -0
- infoman/config/db_vector.py +39 -0
- infoman/config/jwt.py +16 -0
- infoman/config/llm.py +16 -0
- infoman/config/log.py +627 -0
- infoman/config/mq.py +26 -0
- infoman/config/settings.py +65 -0
- infoman/llm/__init__.py +0 -0
- infoman/llm/llm.py +297 -0
- infoman/logger/__init__.py +57 -0
- infoman/logger/context.py +191 -0
- infoman/logger/core.py +358 -0
- infoman/logger/filters.py +157 -0
- infoman/logger/formatters.py +138 -0
- infoman/logger/handlers.py +276 -0
- infoman/logger/metrics.py +160 -0
- infoman/performance/README.md +583 -0
- infoman/performance/__init__.py +19 -0
- infoman/performance/cli.py +215 -0
- infoman/performance/config.py +166 -0
- infoman/performance/reporter.py +519 -0
- infoman/performance/runner.py +303 -0
- infoman/performance/standards.py +222 -0
- infoman/service/__init__.py +8 -0
- infoman/service/app.py +67 -0
- infoman/service/core/__init__.py +0 -0
- infoman/service/core/auth.py +105 -0
- infoman/service/core/lifespan.py +132 -0
- infoman/service/core/monitor.py +57 -0
- infoman/service/core/response.py +37 -0
- infoman/service/exception/__init__.py +7 -0
- infoman/service/exception/error.py +274 -0
- infoman/service/exception/exception.py +25 -0
- infoman/service/exception/handler.py +238 -0
- infoman/service/infrastructure/__init__.py +8 -0
- infoman/service/infrastructure/base.py +212 -0
- infoman/service/infrastructure/db_cache/__init__.py +8 -0
- infoman/service/infrastructure/db_cache/manager.py +194 -0
- infoman/service/infrastructure/db_relation/__init__.py +41 -0
- infoman/service/infrastructure/db_relation/manager.py +300 -0
- infoman/service/infrastructure/db_relation/manager_pro.py +408 -0
- infoman/service/infrastructure/db_relation/mysql.py +52 -0
- infoman/service/infrastructure/db_relation/pgsql.py +54 -0
- infoman/service/infrastructure/db_relation/sqllite.py +25 -0
- infoman/service/infrastructure/db_vector/__init__.py +40 -0
- infoman/service/infrastructure/db_vector/manager.py +201 -0
- infoman/service/infrastructure/db_vector/qdrant.py +322 -0
- infoman/service/infrastructure/mq/__init__.py +15 -0
- infoman/service/infrastructure/mq/manager.py +178 -0
- infoman/service/infrastructure/mq/nats/__init__.py +0 -0
- infoman/service/infrastructure/mq/nats/nats_client.py +57 -0
- infoman/service/infrastructure/mq/nats/nats_event_router.py +25 -0
- infoman/service/launch.py +284 -0
- infoman/service/middleware/__init__.py +7 -0
- infoman/service/middleware/base.py +41 -0
- infoman/service/middleware/logging.py +51 -0
- infoman/service/middleware/rate_limit.py +301 -0
- infoman/service/middleware/request_id.py +21 -0
- infoman/service/middleware/white_list.py +24 -0
- infoman/service/models/__init__.py +8 -0
- infoman/service/models/base.py +441 -0
- infoman/service/models/type/embed.py +70 -0
- infoman/service/routers/__init__.py +18 -0
- infoman/service/routers/health_router.py +311 -0
- infoman/service/routers/monitor_router.py +44 -0
- infoman/service/utils/__init__.py +8 -0
- infoman/service/utils/cache/__init__.py +0 -0
- infoman/service/utils/cache/cache.py +192 -0
- infoman/service/utils/module_loader.py +10 -0
- infoman/service/utils/parse.py +10 -0
- infoman/service/utils/resolver/__init__.py +8 -0
- infoman/service/utils/resolver/base.py +47 -0
- infoman/service/utils/resolver/resp.py +102 -0
- infoman/service/vector/__init__.py +20 -0
- infoman/service/vector/base.py +56 -0
- infoman/service/vector/qdrant.py +125 -0
- infoman/service/vector/service.py +67 -0
- infoman/utils/__init__.py +2 -0
- infoman/utils/decorators/__init__.py +8 -0
- infoman/utils/decorators/cache.py +137 -0
- infoman/utils/decorators/retry.py +99 -0
- infoman/utils/decorators/safe_execute.py +99 -0
- infoman/utils/decorators/timing.py +99 -0
- infoman/utils/encryption/__init__.py +8 -0
- infoman/utils/encryption/aes.py +66 -0
- infoman/utils/encryption/ecc.py +108 -0
- infoman/utils/encryption/rsa.py +112 -0
- infoman/utils/file/__init__.py +0 -0
- infoman/utils/file/handler.py +22 -0
- infoman/utils/hash/__init__.py +0 -0
- infoman/utils/hash/hash.py +61 -0
- infoman/utils/http/__init__.py +8 -0
- infoman/utils/http/client.py +62 -0
- infoman/utils/http/info.py +94 -0
- infoman/utils/http/result.py +19 -0
- infoman/utils/notification/__init__.py +8 -0
- infoman/utils/notification/feishu.py +35 -0
- infoman/utils/text/__init__.py +8 -0
- infoman/utils/text/extractor.py +111 -0
- infomankit-0.3.23.dist-info/METADATA +632 -0
- infomankit-0.3.23.dist-info/RECORD +143 -0
- infomankit-0.3.23.dist-info/WHEEL +4 -0
- infomankit-0.3.23.dist-info/entry_points.txt +5 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
# !/usr/bin/env python
|
|
2
|
+
# -*-coding:utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
健康检查路由(支持可选依赖)
|
|
6
|
+
|
|
7
|
+
提供多层次的健康检查端点,支持 Kubernetes 探针
|
|
8
|
+
- /api/health - 基础健康检查
|
|
9
|
+
- /api/health/liveness - 存活探针
|
|
10
|
+
- /api/health/readiness - 就绪探针
|
|
11
|
+
- /api/health/startup - 启动探针
|
|
12
|
+
- /api/health/detailed - 详细健康状态
|
|
13
|
+
|
|
14
|
+
注意:所有检查都会优雅处理可选依赖(Redis/Vector/NATS)
|
|
15
|
+
未安装的依赖会标记为 "not_configured",不影响整体健康状态
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import time
|
|
19
|
+
from typing import Dict, Any, List
|
|
20
|
+
from fastapi import Request, APIRouter, status
|
|
21
|
+
from loguru import logger
|
|
22
|
+
|
|
23
|
+
from infoman.service.core.response import success, failed
|
|
24
|
+
|
|
25
|
+
health_router = APIRouter(prefix="")
|
|
26
|
+
|
|
27
|
+
# 应用启动时间
|
|
28
|
+
_startup_time = time.time()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@health_router.get(
|
|
32
|
+
summary="基础健康检查",
|
|
33
|
+
path="",
|
|
34
|
+
description="返回应用的基本健康状态",
|
|
35
|
+
)
|
|
36
|
+
async def api_health(req: Request):
|
|
37
|
+
"""基础健康检查 - 仅检查应用是否运行"""
|
|
38
|
+
uptime = time.time() - _startup_time
|
|
39
|
+
return success(data={
|
|
40
|
+
"status": "healthy",
|
|
41
|
+
"uptime_seconds": round(uptime, 2),
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@health_router.get(
|
|
46
|
+
"/liveness",
|
|
47
|
+
summary="存活探针",
|
|
48
|
+
description="Kubernetes liveness probe - 检查应用是否存活",
|
|
49
|
+
status_code=status.HTTP_200_OK,
|
|
50
|
+
)
|
|
51
|
+
async def liveness_check(req: Request):
|
|
52
|
+
"""
|
|
53
|
+
存活探针 (Liveness Probe)
|
|
54
|
+
|
|
55
|
+
用于判断应用是否还活着,如果失败,Kubernetes 会重启 Pod
|
|
56
|
+
这个检查应该非常轻量,只检查应用进程是否响应
|
|
57
|
+
"""
|
|
58
|
+
return {"status": "alive"}
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@health_router.get(
|
|
62
|
+
"/readiness",
|
|
63
|
+
summary="就绪探针",
|
|
64
|
+
description="Kubernetes readiness probe - 检查应用是否就绪接受流量",
|
|
65
|
+
status_code=status.HTTP_200_OK,
|
|
66
|
+
responses={
|
|
67
|
+
200: {"description": "Application is ready"},
|
|
68
|
+
503: {"description": "Application is not ready"},
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
async def readiness_check(req: Request):
|
|
72
|
+
"""
|
|
73
|
+
就绪探针 (Readiness Probe)
|
|
74
|
+
|
|
75
|
+
用于判断应用是否准备好接受流量
|
|
76
|
+
如果失败,Kubernetes 会将 Pod 从 Service 负载均衡中移除
|
|
77
|
+
|
|
78
|
+
检查项:
|
|
79
|
+
- 数据库连接(必需)
|
|
80
|
+
- Redis 连接(可选)
|
|
81
|
+
- 向量数据库连接(可选)
|
|
82
|
+
- 消息队列连接(可选)
|
|
83
|
+
|
|
84
|
+
注意:只有必需的数据库会影响就绪状态,可选依赖不影响
|
|
85
|
+
"""
|
|
86
|
+
checks: Dict[str, Any] = {}
|
|
87
|
+
all_ready = True
|
|
88
|
+
|
|
89
|
+
# 检查数据库(必需)
|
|
90
|
+
try:
|
|
91
|
+
if hasattr(req.app.state, "db_manager"):
|
|
92
|
+
db_health = await req.app.state.db_manager.health_check()
|
|
93
|
+
checks["database"] = db_health
|
|
94
|
+
if db_health.get("status") != "healthy":
|
|
95
|
+
all_ready = False
|
|
96
|
+
else:
|
|
97
|
+
checks["database"] = {"status": "not_configured"}
|
|
98
|
+
all_ready = False # 数据库是必需的
|
|
99
|
+
except Exception as e:
|
|
100
|
+
logger.error(f"Database readiness check failed: {e}")
|
|
101
|
+
checks["database"] = {"status": "unhealthy", "error": str(e)}
|
|
102
|
+
all_ready = False
|
|
103
|
+
|
|
104
|
+
# 检查 Redis(可选,不影响就绪状态)
|
|
105
|
+
try:
|
|
106
|
+
if hasattr(req.app.state, "redis_manager"):
|
|
107
|
+
redis_health = await req.app.state.redis_manager.health_check()
|
|
108
|
+
checks["redis"] = redis_health
|
|
109
|
+
# 可选依赖不影响就绪状态
|
|
110
|
+
else:
|
|
111
|
+
checks["redis"] = {"status": "not_configured"}
|
|
112
|
+
except Exception as e:
|
|
113
|
+
logger.warning(f"Redis readiness check failed: {e}")
|
|
114
|
+
checks["redis"] = {"status": "unhealthy", "error": str(e)}
|
|
115
|
+
|
|
116
|
+
# 检查向量数据库(可选,不影响就绪状态)
|
|
117
|
+
try:
|
|
118
|
+
if hasattr(req.app.state, "vector_manager"):
|
|
119
|
+
vector_health = await req.app.state.vector_manager.health_check()
|
|
120
|
+
checks["vector_db"] = vector_health
|
|
121
|
+
# 可选依赖不影响就绪状态
|
|
122
|
+
else:
|
|
123
|
+
checks["vector_db"] = {"status": "not_configured"}
|
|
124
|
+
except Exception as e:
|
|
125
|
+
logger.warning(f"Vector DB readiness check failed: {e}")
|
|
126
|
+
checks["vector_db"] = {"status": "unhealthy", "error": str(e)}
|
|
127
|
+
|
|
128
|
+
# 检查消息队列(可选,不影响就绪状态)
|
|
129
|
+
try:
|
|
130
|
+
if hasattr(req.app.state, "nats_manager"):
|
|
131
|
+
nats_health = await req.app.state.nats_manager.health_check()
|
|
132
|
+
checks["message_queue"] = nats_health
|
|
133
|
+
# 可选依赖不影响就绪状态
|
|
134
|
+
else:
|
|
135
|
+
checks["message_queue"] = {"status": "not_configured"}
|
|
136
|
+
except Exception as e:
|
|
137
|
+
logger.warning(f"NATS readiness check failed: {e}")
|
|
138
|
+
checks["message_queue"] = {"status": "unhealthy", "error": str(e)}
|
|
139
|
+
|
|
140
|
+
if all_ready:
|
|
141
|
+
return {
|
|
142
|
+
"status": "ready",
|
|
143
|
+
"checks": checks,
|
|
144
|
+
}
|
|
145
|
+
else:
|
|
146
|
+
return JSONResponse(
|
|
147
|
+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
148
|
+
content={
|
|
149
|
+
"status": "not_ready",
|
|
150
|
+
"checks": checks,
|
|
151
|
+
},
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
@health_router.get(
|
|
156
|
+
"/startup",
|
|
157
|
+
summary="启动探针",
|
|
158
|
+
description="Kubernetes startup probe - 检查应用是否已完成启动",
|
|
159
|
+
status_code=status.HTTP_200_OK,
|
|
160
|
+
responses={
|
|
161
|
+
200: {"description": "Application has started"},
|
|
162
|
+
503: {"description": "Application is still starting"},
|
|
163
|
+
},
|
|
164
|
+
)
|
|
165
|
+
async def startup_check(req: Request):
|
|
166
|
+
"""
|
|
167
|
+
启动探针 (Startup Probe)
|
|
168
|
+
|
|
169
|
+
用于检查应用是否已经完成启动
|
|
170
|
+
适用于启动缓慢的应用,避免 liveness probe 过早杀死容器
|
|
171
|
+
|
|
172
|
+
启动完成标准:
|
|
173
|
+
- 应用运行超过最小启动时间
|
|
174
|
+
- 所有关键服务已初始化
|
|
175
|
+
"""
|
|
176
|
+
uptime = time.time() - _startup_time
|
|
177
|
+
min_startup_time = 5 # 最小启动时间(秒)
|
|
178
|
+
|
|
179
|
+
if uptime < min_startup_time:
|
|
180
|
+
return JSONResponse(
|
|
181
|
+
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
|
|
182
|
+
content={
|
|
183
|
+
"status": "starting",
|
|
184
|
+
"uptime_seconds": round(uptime, 2),
|
|
185
|
+
"message": f"Application is still starting (min: {min_startup_time}s)",
|
|
186
|
+
},
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# 检查关键服务是否已初始化
|
|
190
|
+
checks = {}
|
|
191
|
+
|
|
192
|
+
if hasattr(req.app.state, "db_manager"):
|
|
193
|
+
checks["database"] = "initialized"
|
|
194
|
+
if hasattr(req.app.state, "redis_manager"):
|
|
195
|
+
checks["redis"] = "initialized"
|
|
196
|
+
if hasattr(req.app.state, "vector_manager"):
|
|
197
|
+
checks["vector_db"] = "initialized"
|
|
198
|
+
if hasattr(req.app.state, "nats_manager"):
|
|
199
|
+
checks["message_queue"] = "initialized"
|
|
200
|
+
|
|
201
|
+
return {
|
|
202
|
+
"status": "started",
|
|
203
|
+
"uptime_seconds": round(uptime, 2),
|
|
204
|
+
"initialized_services": checks,
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
@health_router.get(
|
|
209
|
+
"/detailed",
|
|
210
|
+
summary="详细健康状态",
|
|
211
|
+
description="返回所有组件的详细健康状态",
|
|
212
|
+
)
|
|
213
|
+
async def detailed_health(req: Request):
|
|
214
|
+
"""
|
|
215
|
+
详细健康检查
|
|
216
|
+
|
|
217
|
+
返回应用和所有依赖服务的详细健康状态
|
|
218
|
+
包括版本信息、连接状态、性能指标等
|
|
219
|
+
"""
|
|
220
|
+
from infoman.config.settings import settings
|
|
221
|
+
import psutil
|
|
222
|
+
|
|
223
|
+
uptime = time.time() - _startup_time
|
|
224
|
+
|
|
225
|
+
health_data: Dict[str, Any] = {
|
|
226
|
+
"status": "healthy",
|
|
227
|
+
"timestamp": time.time(),
|
|
228
|
+
"uptime_seconds": round(uptime, 2),
|
|
229
|
+
"application": {
|
|
230
|
+
"name": settings.APP_NAME,
|
|
231
|
+
"version": settings.APP_VERSION,
|
|
232
|
+
"environment": settings.ENV,
|
|
233
|
+
},
|
|
234
|
+
"system": {
|
|
235
|
+
"cpu_percent": psutil.cpu_percent(interval=0.1),
|
|
236
|
+
"memory_percent": psutil.virtual_memory().percent,
|
|
237
|
+
"disk_percent": psutil.disk_usage('/').percent,
|
|
238
|
+
},
|
|
239
|
+
"dependencies": {},
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
# 收集所有依赖的健康状态
|
|
243
|
+
dependencies_health: List[Dict[str, Any]] = []
|
|
244
|
+
|
|
245
|
+
# 数据库
|
|
246
|
+
if hasattr(req.app.state, "db_manager"):
|
|
247
|
+
try:
|
|
248
|
+
db_health = await req.app.state.db_manager.health_check()
|
|
249
|
+
dependencies_health.append(db_health)
|
|
250
|
+
health_data["dependencies"]["database"] = db_health
|
|
251
|
+
except Exception as e:
|
|
252
|
+
logger.error(f"Database health check failed: {e}")
|
|
253
|
+
health_data["dependencies"]["database"] = {
|
|
254
|
+
"status": "error",
|
|
255
|
+
"error": str(e),
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
# Redis
|
|
259
|
+
if hasattr(req.app.state, "redis_manager"):
|
|
260
|
+
try:
|
|
261
|
+
redis_health = await req.app.state.redis_manager.health_check()
|
|
262
|
+
dependencies_health.append(redis_health)
|
|
263
|
+
health_data["dependencies"]["redis"] = redis_health
|
|
264
|
+
except Exception as e:
|
|
265
|
+
logger.error(f"Redis health check failed: {e}")
|
|
266
|
+
health_data["dependencies"]["redis"] = {
|
|
267
|
+
"status": "error",
|
|
268
|
+
"error": str(e),
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
# 向量数据库
|
|
272
|
+
if hasattr(req.app.state, "vector_manager"):
|
|
273
|
+
try:
|
|
274
|
+
vector_health = await req.app.state.vector_manager.health_check()
|
|
275
|
+
dependencies_health.append(vector_health)
|
|
276
|
+
health_data["dependencies"]["vector_db"] = vector_health
|
|
277
|
+
except Exception as e:
|
|
278
|
+
logger.error(f"Vector DB health check failed: {e}")
|
|
279
|
+
health_data["dependencies"]["vector_db"] = {
|
|
280
|
+
"status": "error",
|
|
281
|
+
"error": str(e),
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
# 消息队列
|
|
285
|
+
if hasattr(req.app.state, "nats_manager"):
|
|
286
|
+
try:
|
|
287
|
+
nats_health = await req.app.state.nats_manager.health_check()
|
|
288
|
+
dependencies_health.append(nats_health)
|
|
289
|
+
health_data["dependencies"]["message_queue"] = nats_health
|
|
290
|
+
except Exception as e:
|
|
291
|
+
logger.error(f"NATS health check failed: {e}")
|
|
292
|
+
health_data["dependencies"]["message_queue"] = {
|
|
293
|
+
"status": "error",
|
|
294
|
+
"error": str(e),
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
# 判断整体健康状态
|
|
298
|
+
unhealthy_count = sum(
|
|
299
|
+
1 for dep in dependencies_health
|
|
300
|
+
if dep.get("status") not in ["healthy", "not_configured"]
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
if unhealthy_count > 0:
|
|
304
|
+
health_data["status"] = "degraded"
|
|
305
|
+
health_data["unhealthy_dependencies"] = unhealthy_count
|
|
306
|
+
|
|
307
|
+
return success(data=health_data)
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
# 导入 JSONResponse
|
|
311
|
+
from fastapi.responses import JSONResponse
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# !/usr/bin/env python
|
|
2
|
+
# -*-coding:utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
# Time :2024/2/2 20:05
|
|
6
|
+
# Author :Maxwell
|
|
7
|
+
# Description:
|
|
8
|
+
"""
|
|
9
|
+
import pytz
|
|
10
|
+
import psutil
|
|
11
|
+
from fastapi import APIRouter
|
|
12
|
+
from infoman.service.core.response import success
|
|
13
|
+
from infoman.config.settings import settings
|
|
14
|
+
from datetime import datetime
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
monitor_router = APIRouter(prefix="")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
@monitor_router.get(path="/info")
|
|
21
|
+
async def monitor():
|
|
22
|
+
cpu_percent = psutil.cpu_percent(interval=0.1)
|
|
23
|
+
memory = psutil.virtual_memory()
|
|
24
|
+
process = psutil.Process()
|
|
25
|
+
process_memory = process.memory_info().rss / (1024 * 1024)
|
|
26
|
+
return success(
|
|
27
|
+
{
|
|
28
|
+
"app": {
|
|
29
|
+
"version": settings.APP_VERSION,
|
|
30
|
+
"name": settings.APP_NAME,
|
|
31
|
+
"env": settings.ENV,
|
|
32
|
+
},
|
|
33
|
+
"system": {
|
|
34
|
+
"cpu_percent": f"{cpu_percent:.3f}",
|
|
35
|
+
"memory_percent": f"{memory.percent:.3f}",
|
|
36
|
+
"memory_available_mb": int(memory.available / (1024 * 1024)),
|
|
37
|
+
},
|
|
38
|
+
"process": {
|
|
39
|
+
"memory_mb": int(process_memory),
|
|
40
|
+
"cpu_percent": f"'{process.cpu_percent(interval=0.1):.3f}",
|
|
41
|
+
},
|
|
42
|
+
"timestamp": datetime.now(pytz.UTC).isoformat(),
|
|
43
|
+
}
|
|
44
|
+
)
|
|
File without changes
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
from infoman.logger import logger
|
|
2
|
+
from functools import wraps
|
|
3
|
+
from typing import (
|
|
4
|
+
Any,
|
|
5
|
+
Callable,
|
|
6
|
+
Optional,
|
|
7
|
+
Type,
|
|
8
|
+
TypeVar,
|
|
9
|
+
get_type_hints,
|
|
10
|
+
get_origin,
|
|
11
|
+
get_args,
|
|
12
|
+
)
|
|
13
|
+
import json
|
|
14
|
+
from pydantic import BaseModel
|
|
15
|
+
import inspect
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
T = TypeVar("T")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def redis_cache(
|
|
22
|
+
prefix: str,
|
|
23
|
+
ttl: int = 3600,
|
|
24
|
+
model: Optional[Type[BaseModel]] = None, # ✅ 新增:指定返回模型
|
|
25
|
+
auto_detect: bool = True, # ✅ 新增:自动检测返回类型
|
|
26
|
+
):
|
|
27
|
+
"""
|
|
28
|
+
Redis 缓存装饰器(支持 Pydantic 模型反序列化)
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
prefix: 缓存键前缀
|
|
32
|
+
ttl: 过期时间(秒)
|
|
33
|
+
model: 返回的 Pydantic 模型类(可选,如果指定则强制使用)
|
|
34
|
+
auto_detect: 是否自动从函数签名检测返回类型
|
|
35
|
+
|
|
36
|
+
Examples:
|
|
37
|
+
# 方式 1:手动指定模型
|
|
38
|
+
@redis_cache(prefix="config", ttl=3600, model=ConfigKeyLogSchema)
|
|
39
|
+
async def get_config(pub_key: str):
|
|
40
|
+
...
|
|
41
|
+
|
|
42
|
+
# 方式 2:自动检测(推荐)
|
|
43
|
+
@redis_cache(prefix="config", ttl=3600)
|
|
44
|
+
async def get_config(pub_key: str) -> ConfigKeyLogSchema:
|
|
45
|
+
...
|
|
46
|
+
|
|
47
|
+
# 方式 3:列表类型
|
|
48
|
+
@redis_cache(prefix="configs", ttl=3600)
|
|
49
|
+
async def list_configs() -> list[ConfigKeyLogSchema]:
|
|
50
|
+
...
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
def decorator(func: Callable) -> Callable:
|
|
54
|
+
# ✅ 自动检测返回类型
|
|
55
|
+
return_type = model
|
|
56
|
+
is_list = False
|
|
57
|
+
|
|
58
|
+
if auto_detect and not return_type:
|
|
59
|
+
type_hints = get_type_hints(func)
|
|
60
|
+
if "return" in type_hints:
|
|
61
|
+
hint = type_hints["return"]
|
|
62
|
+
|
|
63
|
+
# 处理 Optional[Model]
|
|
64
|
+
origin = get_origin(hint)
|
|
65
|
+
if origin is Optional or str(origin) == "typing.Union":
|
|
66
|
+
args = get_args(hint)
|
|
67
|
+
for arg in args:
|
|
68
|
+
if arg is not type(None) and (
|
|
69
|
+
isinstance(arg, type) and issubclass(arg, BaseModel)
|
|
70
|
+
):
|
|
71
|
+
return_type = arg
|
|
72
|
+
break
|
|
73
|
+
elif origin is list:
|
|
74
|
+
args = get_args(hint)
|
|
75
|
+
if (
|
|
76
|
+
args
|
|
77
|
+
and isinstance(args[0], type)
|
|
78
|
+
and issubclass(args[0], BaseModel)
|
|
79
|
+
):
|
|
80
|
+
return_type = args[0]
|
|
81
|
+
is_list = True
|
|
82
|
+
|
|
83
|
+
# 直接是 Model
|
|
84
|
+
elif isinstance(hint, type) and issubclass(hint, BaseModel):
|
|
85
|
+
return_type = hint
|
|
86
|
+
|
|
87
|
+
@wraps(func)
|
|
88
|
+
async def wrapper(*args, **kwargs) -> Any:
|
|
89
|
+
# 获取 Redis 客户端
|
|
90
|
+
redis_client = _get_redis_client(args, kwargs)
|
|
91
|
+
|
|
92
|
+
if not redis_client:
|
|
93
|
+
logger.warning("Redis client not found, executing without cache")
|
|
94
|
+
return await func(*args, **kwargs)
|
|
95
|
+
|
|
96
|
+
# 构建缓存键
|
|
97
|
+
cache_key = f"{prefix}:{_build_cache_key(func, args, kwargs)}"
|
|
98
|
+
|
|
99
|
+
# ==================== 读取缓存 ====================
|
|
100
|
+
try:
|
|
101
|
+
cached = await redis_client.get(cache_key)
|
|
102
|
+
if cached:
|
|
103
|
+
cached_data = json.loads(cached)
|
|
104
|
+
logger.debug(f"Cache HIT: {cache_key}")
|
|
105
|
+
|
|
106
|
+
if return_type:
|
|
107
|
+
if is_list:
|
|
108
|
+
return [return_type(**item) for item in cached_data]
|
|
109
|
+
else:
|
|
110
|
+
return return_type(**cached_data)
|
|
111
|
+
return cached_data
|
|
112
|
+
|
|
113
|
+
except Exception as e:
|
|
114
|
+
logger.error(f"Cache read error for {cache_key}: {e}")
|
|
115
|
+
|
|
116
|
+
# ==================== 执行函数 ====================
|
|
117
|
+
result = await func(*args, **kwargs)
|
|
118
|
+
|
|
119
|
+
if result is None:
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
# ==================== 写入缓存 ====================
|
|
123
|
+
try:
|
|
124
|
+
cache_data = _serialize_result(result)
|
|
125
|
+
json_str = json.dumps(cache_data, default=str, ensure_ascii=False)
|
|
126
|
+
await redis_client.setex(cache_key, ttl, json_str)
|
|
127
|
+
logger.debug(f"Cache SET: {cache_key}")
|
|
128
|
+
|
|
129
|
+
except Exception as e:
|
|
130
|
+
logger.error(f"Cache write error for {cache_key}: {e}")
|
|
131
|
+
|
|
132
|
+
return result
|
|
133
|
+
|
|
134
|
+
return wrapper
|
|
135
|
+
|
|
136
|
+
return decorator
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _serialize_result(result: Any) -> Any:
|
|
140
|
+
"""序列化结果"""
|
|
141
|
+
if isinstance(result, BaseModel):
|
|
142
|
+
return result.model_dump()
|
|
143
|
+
elif isinstance(result, list):
|
|
144
|
+
return [
|
|
145
|
+
item.model_dump() if isinstance(item, BaseModel) else item
|
|
146
|
+
for item in result
|
|
147
|
+
]
|
|
148
|
+
elif isinstance(result, dict):
|
|
149
|
+
return {
|
|
150
|
+
k: v.model_dump() if isinstance(v, BaseModel) else v
|
|
151
|
+
for k, v in result.items()
|
|
152
|
+
}
|
|
153
|
+
return result
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _get_redis_client(args: tuple, kwargs: dict):
|
|
157
|
+
|
|
158
|
+
for arg in list(args) + list(kwargs.values()):
|
|
159
|
+
if hasattr(arg, "app") and hasattr(arg.app, "state"):
|
|
160
|
+
redis = getattr(arg.app.state, "redis_client", None)
|
|
161
|
+
if redis:
|
|
162
|
+
return redis
|
|
163
|
+
|
|
164
|
+
return None
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def _build_cache_key(func: Callable, args: tuple, kwargs: dict) -> str:
|
|
168
|
+
"""构建缓存键"""
|
|
169
|
+
key_parts = [func.__name__]
|
|
170
|
+
|
|
171
|
+
# 获取函数签名
|
|
172
|
+
sig = inspect.signature(func)
|
|
173
|
+
param_names = list(sig.parameters.keys())
|
|
174
|
+
|
|
175
|
+
# 处理位置参数(跳过 self 和 request)
|
|
176
|
+
for i, arg in enumerate(args):
|
|
177
|
+
if i == 0 and param_names and param_names[0] in ["self", "cls"]:
|
|
178
|
+
continue
|
|
179
|
+
if _is_cacheable_value(arg):
|
|
180
|
+
key_parts.append(str(arg))
|
|
181
|
+
|
|
182
|
+
# 处理关键字参数
|
|
183
|
+
for k, v in sorted(kwargs.items()):
|
|
184
|
+
if k not in ["req", "request"] and _is_cacheable_value(v):
|
|
185
|
+
key_parts.append(f"{k}={v}")
|
|
186
|
+
|
|
187
|
+
return ":".join(key_parts)
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _is_cacheable_value(value: Any) -> bool:
|
|
191
|
+
"""判断值是否可用于缓存键"""
|
|
192
|
+
return isinstance(value, (str, int, float, bool))
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def auto_import_modules_from_folder(package: str, folder: str):
|
|
6
|
+
for file in Path(folder).glob("*.py"):
|
|
7
|
+
if file.name.startswith("_"):
|
|
8
|
+
continue
|
|
9
|
+
module_name = f"{package}.{file.stem}"
|
|
10
|
+
importlib.import_module(module_name)
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# !/usr/bin/env python
|
|
2
|
+
# -*-coding:utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
# Time :2025/7/9 16:10
|
|
6
|
+
# Author :Maxwell
|
|
7
|
+
# Description:
|
|
8
|
+
"""
|
|
9
|
+
from pydantic import BaseModel
|
|
10
|
+
from datetime import datetime
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class BaseResp(BaseModel):
|
|
14
|
+
class Config:
|
|
15
|
+
arbitrary_types_allowed = True
|
|
16
|
+
|
|
17
|
+
def _format_price(self, value: float) -> str:
|
|
18
|
+
"""格式化价格显示(带两位小数)"""
|
|
19
|
+
if value:
|
|
20
|
+
return f"{value:.2f} 元"
|
|
21
|
+
return ""
|
|
22
|
+
|
|
23
|
+
def _format_percentage(self, value: float) -> str:
|
|
24
|
+
"""格式化百分比显示(带百分号)"""
|
|
25
|
+
if value:
|
|
26
|
+
return f"{value:.2%}"
|
|
27
|
+
return ""
|
|
28
|
+
|
|
29
|
+
def _format_volume(self, value: int) -> str:
|
|
30
|
+
"""格式化成交量显示(带千位分隔符)"""
|
|
31
|
+
if value:
|
|
32
|
+
return f"{value:,} 股"
|
|
33
|
+
return ""
|
|
34
|
+
|
|
35
|
+
def _format_amount(self, value: float) -> str:
|
|
36
|
+
"""格式化成交额显示(带千位分隔符)"""
|
|
37
|
+
if value:
|
|
38
|
+
return f"{value:,} 元"
|
|
39
|
+
return ""
|
|
40
|
+
|
|
41
|
+
def _format_time(self, timestamp: int) -> str:
|
|
42
|
+
"""将时间戳转换为可读时间"""
|
|
43
|
+
if timestamp:
|
|
44
|
+
return datetime.fromtimestamp(timestamp / 1000).strftime(
|
|
45
|
+
"%Y-%m-%d %H:%M:%S"
|
|
46
|
+
)
|
|
47
|
+
return ""
|