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,178 @@
|
|
|
1
|
+
# !/usr/bin/env python
|
|
2
|
+
# -*-coding:utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
消息队列管理器(支持延迟导入)
|
|
6
|
+
|
|
7
|
+
支持:
|
|
8
|
+
- NATS
|
|
9
|
+
- 其他消息队列(待实现)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from typing import Optional, Dict, Any, TYPE_CHECKING
|
|
13
|
+
from fastapi import FastAPI
|
|
14
|
+
from loguru import logger
|
|
15
|
+
|
|
16
|
+
from infoman.config import settings
|
|
17
|
+
|
|
18
|
+
if TYPE_CHECKING:
|
|
19
|
+
from infoman.service.infrastructure.mq.nats.nats_client import NATSClient
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class NATSManager:
|
|
23
|
+
"""NATS 消息队列管理器"""
|
|
24
|
+
|
|
25
|
+
def __init__(self):
|
|
26
|
+
self.nats_client: Optional[Any] = None
|
|
27
|
+
self.initialized = False
|
|
28
|
+
|
|
29
|
+
@property
|
|
30
|
+
def is_available(self) -> bool:
|
|
31
|
+
"""是否可用"""
|
|
32
|
+
return self.nats_client is not None and self.nats_client.connected
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def client(self):
|
|
36
|
+
"""获取 NATS 客户端"""
|
|
37
|
+
return self.nats_client
|
|
38
|
+
|
|
39
|
+
async def startup(self, app: Optional[FastAPI] = None) -> bool:
|
|
40
|
+
"""
|
|
41
|
+
启动 NATS 连接
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
app: FastAPI 应用实例(可选)
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
是否成功启动
|
|
48
|
+
"""
|
|
49
|
+
if self.initialized:
|
|
50
|
+
logger.warning("⚠️ NATSManager 已初始化,跳过重复初始化")
|
|
51
|
+
return True
|
|
52
|
+
|
|
53
|
+
if not settings.NATS_SERVERS:
|
|
54
|
+
logger.info("⏭️ NATS 未配置,跳过初始化")
|
|
55
|
+
return False
|
|
56
|
+
|
|
57
|
+
# 延迟导入 NATSClient
|
|
58
|
+
try:
|
|
59
|
+
from infoman.service.infrastructure.mq.nats.nats_client import NATSClient
|
|
60
|
+
except ImportError as e:
|
|
61
|
+
logger.error(f"❌ NATS 依赖未安装: {e}")
|
|
62
|
+
logger.error("请运行: pip install infomankit[messaging]")
|
|
63
|
+
return False
|
|
64
|
+
|
|
65
|
+
logger.info("🚀 初始化 NATS...")
|
|
66
|
+
|
|
67
|
+
try:
|
|
68
|
+
self.nats_client = NATSClient(
|
|
69
|
+
servers=settings.NATS_SERVERS,
|
|
70
|
+
name=settings.APP_NAME
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
# 连接到 NATS
|
|
74
|
+
await self.nats_client.connect()
|
|
75
|
+
|
|
76
|
+
# 挂载到 app.state(如果提供了 app)
|
|
77
|
+
if app:
|
|
78
|
+
app.state.nats_client = self.nats_client
|
|
79
|
+
logger.debug("✅ NATS 客户端已挂载到 app.state")
|
|
80
|
+
|
|
81
|
+
self.initialized = True
|
|
82
|
+
logger.success(f"✅ NATS 连接成功: {settings.NATS_SERVERS}")
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
except Exception as e:
|
|
86
|
+
logger.error(f"❌ NATS 连接失败: {e}")
|
|
87
|
+
return False
|
|
88
|
+
|
|
89
|
+
async def shutdown(self):
|
|
90
|
+
"""关闭 NATS 连接"""
|
|
91
|
+
if not self.initialized:
|
|
92
|
+
return
|
|
93
|
+
|
|
94
|
+
logger.info("⏹️ 关闭 NATS 连接...")
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
if self.nats_client:
|
|
98
|
+
await self.nats_client.close()
|
|
99
|
+
logger.success("✅ NATS 连接已关闭")
|
|
100
|
+
|
|
101
|
+
except Exception as e:
|
|
102
|
+
logger.error(f"❌ NATS 关闭失败: {e}")
|
|
103
|
+
|
|
104
|
+
finally:
|
|
105
|
+
self.initialized = False
|
|
106
|
+
|
|
107
|
+
async def health_check(self) -> Dict[str, Any]:
|
|
108
|
+
"""
|
|
109
|
+
健康检查
|
|
110
|
+
|
|
111
|
+
Returns:
|
|
112
|
+
{
|
|
113
|
+
"status": "healthy" | "unhealthy" | "not_configured",
|
|
114
|
+
"name": "nats",
|
|
115
|
+
"details": {...}
|
|
116
|
+
}
|
|
117
|
+
"""
|
|
118
|
+
if not settings.NATS_SERVER:
|
|
119
|
+
return {
|
|
120
|
+
"status": "not_configured",
|
|
121
|
+
"name": "nats",
|
|
122
|
+
"details": {"enabled": False}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if not self.initialized or not self.nats_client:
|
|
126
|
+
return {
|
|
127
|
+
"status": "unhealthy",
|
|
128
|
+
"name": "nats",
|
|
129
|
+
"details": {"error": "未初始化"}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
try:
|
|
133
|
+
# 检查连接状态
|
|
134
|
+
is_connected = self.nats_client.connected
|
|
135
|
+
|
|
136
|
+
if is_connected:
|
|
137
|
+
return {
|
|
138
|
+
"status": "healthy",
|
|
139
|
+
"name": "nats",
|
|
140
|
+
"details": {
|
|
141
|
+
"connected": True,
|
|
142
|
+
"servers": self.nats_client.servers
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
else:
|
|
146
|
+
return {
|
|
147
|
+
"status": "unhealthy",
|
|
148
|
+
"name": "nats",
|
|
149
|
+
"details": {
|
|
150
|
+
"connected": False,
|
|
151
|
+
"error": "连接已断开"
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
except Exception as e:
|
|
156
|
+
return {
|
|
157
|
+
"status": "unhealthy",
|
|
158
|
+
"name": "nats",
|
|
159
|
+
"details": {"error": str(e)}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
async def get_stats(self) -> Dict[str, Any]:
|
|
163
|
+
"""
|
|
164
|
+
获取统计信息
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
{
|
|
168
|
+
"connected": bool,
|
|
169
|
+
"servers": list,
|
|
170
|
+
}
|
|
171
|
+
"""
|
|
172
|
+
if not self.is_available:
|
|
173
|
+
return {}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
"connected": self.nats_client.connected,
|
|
177
|
+
"servers": self.nats_client.servers,
|
|
178
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
from nats.aio.client import Client as NATS
|
|
4
|
+
from nats.aio.errors import ErrConnectionClosed, ErrTimeout, ErrNoServers
|
|
5
|
+
from infoman.logger import logger
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class NATSClient:
|
|
9
|
+
|
|
10
|
+
def __init__(self, servers=None, name="nats-client"):
|
|
11
|
+
if servers is None:
|
|
12
|
+
servers = ["nats://127.0.0.1:4222"]
|
|
13
|
+
self.nc = NATS()
|
|
14
|
+
self.servers = servers
|
|
15
|
+
self.name = name
|
|
16
|
+
self.connected = False
|
|
17
|
+
|
|
18
|
+
async def connect(self):
|
|
19
|
+
if not self.connected:
|
|
20
|
+
await self.nc.connect(servers=self.servers, name=self.name)
|
|
21
|
+
self.connected = True
|
|
22
|
+
logger.info(f"[NATS] Connected to {self.servers}")
|
|
23
|
+
|
|
24
|
+
async def publish(self, subject: str, message: dict):
|
|
25
|
+
if not self.connected:
|
|
26
|
+
await self.connect()
|
|
27
|
+
try:
|
|
28
|
+
payload = json.dumps(message).encode()
|
|
29
|
+
await self.nc.publish(subject, payload)
|
|
30
|
+
logger.info(f"[NATS] Published to {subject}: {message}")
|
|
31
|
+
except Exception as e:
|
|
32
|
+
logger.info(f"[NATS] Publish error: {e}")
|
|
33
|
+
|
|
34
|
+
async def request(self, subject: str, message: dict, timeout=1.0):
|
|
35
|
+
if not self.connected:
|
|
36
|
+
await self.connect()
|
|
37
|
+
try:
|
|
38
|
+
payload = json.dumps(message).encode()
|
|
39
|
+
msg = await self.nc.request(subject, payload, timeout=timeout)
|
|
40
|
+
return json.loads(msg.data.decode())
|
|
41
|
+
except ErrTimeout:
|
|
42
|
+
logger.info("[NATS] Request timeout")
|
|
43
|
+
return None
|
|
44
|
+
except Exception as e:
|
|
45
|
+
logger.info(f"[NATS] Request error: {e}")
|
|
46
|
+
return None
|
|
47
|
+
|
|
48
|
+
async def subscribe(self, subject: str, callback, queue: str):
|
|
49
|
+
if not self.connected:
|
|
50
|
+
await self.connect()
|
|
51
|
+
await self.nc.subscribe(subject, cb=callback, queue=queue)
|
|
52
|
+
|
|
53
|
+
async def close(self):
|
|
54
|
+
if self.connected:
|
|
55
|
+
await self.nc.drain()
|
|
56
|
+
self.connected = False
|
|
57
|
+
logger.info("[NATS] Connection closed")
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from infoman.logger import logger
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class EventRouter:
|
|
5
|
+
def __init__(self):
|
|
6
|
+
self.routes = {}
|
|
7
|
+
|
|
8
|
+
def on(self, subject, queue=None):
|
|
9
|
+
def decorator(func):
|
|
10
|
+
self.routes[subject] = {"handler": func, "queue": queue}
|
|
11
|
+
return func
|
|
12
|
+
|
|
13
|
+
return decorator
|
|
14
|
+
|
|
15
|
+
async def register(self, nats_client):
|
|
16
|
+
for subject, meta in self.routes.items():
|
|
17
|
+
|
|
18
|
+
async def handler(msg, func=meta["handler"]):
|
|
19
|
+
await func(msg, nats_cli=nats_client)
|
|
20
|
+
|
|
21
|
+
await nats_client.subscribe(subject, handler, meta["queue"])
|
|
22
|
+
logger.info(f"[Router] Bound {meta['handler'].__name__} to '{subject}'")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
event_router = EventRouter()
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
# -*-coding:utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
应用启动入口(库模式)
|
|
6
|
+
|
|
7
|
+
作为基础库使用时,支持:
|
|
8
|
+
1. 直接启动内置应用:python -m infoman.service.launch
|
|
9
|
+
2. 启动用户应用:python -m infoman.service.launch --app your_module:app
|
|
10
|
+
3. 作为库函数调用:from infoman.service.launch import serve
|
|
11
|
+
|
|
12
|
+
支持多种 ASGI 服务器:
|
|
13
|
+
- granian (推荐生产环境,Rust 实现,性能最佳)
|
|
14
|
+
- uvicorn (开发环境友好,热重载)
|
|
15
|
+
- gunicorn (传统部署)
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
import os
|
|
19
|
+
from typing import Optional, Dict, Any
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def serve(
|
|
23
|
+
app_target: str = "infoman.service.app:application",
|
|
24
|
+
server: str = "granian",
|
|
25
|
+
host: Optional[str] = None,
|
|
26
|
+
port: Optional[int] = None,
|
|
27
|
+
workers: Optional[int] = None,
|
|
28
|
+
log_level: Optional[str] = None,
|
|
29
|
+
**kwargs
|
|
30
|
+
):
|
|
31
|
+
"""
|
|
32
|
+
启动 ASGI 应用服务器(库函数)
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
app_target: 应用目标 (格式: "module.path:app_instance")
|
|
36
|
+
server: 服务器类型 (granian/uvicorn/gunicorn)
|
|
37
|
+
host: 监听地址
|
|
38
|
+
port: 监听端口
|
|
39
|
+
workers: 工作进程数
|
|
40
|
+
threads: 线程数(仅 Granian)
|
|
41
|
+
reload: 是否启用热重载
|
|
42
|
+
log_level: 日志级别
|
|
43
|
+
**kwargs: 其他服务器特定参数
|
|
44
|
+
|
|
45
|
+
Examples:
|
|
46
|
+
>>> # 启动默认应用
|
|
47
|
+
>>> from infoman.service.launch import serve
|
|
48
|
+
>>> serve()
|
|
49
|
+
|
|
50
|
+
>>> # 启动自定义应用
|
|
51
|
+
>>> serve(app_target="myapp.main:app", port=8080)
|
|
52
|
+
|
|
53
|
+
>>> # 生产环境配置
|
|
54
|
+
>>> serve(
|
|
55
|
+
... app_target="myapp.main:app",
|
|
56
|
+
... server="granian",
|
|
57
|
+
... workers=4,
|
|
58
|
+
... reload=False,
|
|
59
|
+
... log_level="info"
|
|
60
|
+
... )
|
|
61
|
+
"""
|
|
62
|
+
# 导入配置(优先使用参数,其次使用配置文件)
|
|
63
|
+
try:
|
|
64
|
+
from infoman.config import settings
|
|
65
|
+
except ImportError:
|
|
66
|
+
settings = None
|
|
67
|
+
|
|
68
|
+
# 参数优先级:函数参数 > 配置文件 > 默认值
|
|
69
|
+
# 注意:在 macOS 上使用 Granian 时,0.0.0.0 可能导致 "Can't assign requested address" 错误
|
|
70
|
+
import platform
|
|
71
|
+
|
|
72
|
+
# 确定主机地址
|
|
73
|
+
if host:
|
|
74
|
+
# 用户明确指定,直接使用
|
|
75
|
+
resolved_host = host
|
|
76
|
+
elif settings and settings.APP_HOST != "0.0.0.0":
|
|
77
|
+
# 配置文件中有非默认值,使用配置
|
|
78
|
+
resolved_host = settings.APP_HOST
|
|
79
|
+
else:
|
|
80
|
+
# 使用平台相关的默认值
|
|
81
|
+
if server == "granian" and platform.system() == "Darwin":
|
|
82
|
+
# macOS + Granian: 使用 127.0.0.1
|
|
83
|
+
resolved_host = "127.0.0.1"
|
|
84
|
+
else:
|
|
85
|
+
# 其他情况:使用 0.0.0.0
|
|
86
|
+
resolved_host = "0.0.0.0"
|
|
87
|
+
|
|
88
|
+
config = {
|
|
89
|
+
"host": resolved_host,
|
|
90
|
+
"port": port or (settings.APP_PORT if settings else 8000),
|
|
91
|
+
"workers": workers or (settings.APP_WORKERS if settings and hasattr(settings, "APP_WORKERS") else 2),
|
|
92
|
+
"log_level": log_level or (settings.LOG_LEVEL.lower() if settings and hasattr(settings, "LOG_LEVEL") else "info"),
|
|
93
|
+
"app_name": settings.APP_NAME if settings else "Application",
|
|
94
|
+
"env": settings.ENV if settings else "unknown",
|
|
95
|
+
"docs_url": settings.DOCS_URL if settings and hasattr(settings, "DOCS_URL") else "/docs",
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# 合并 kwargs
|
|
99
|
+
config.update(kwargs)
|
|
100
|
+
|
|
101
|
+
# 根据服务器类型启动
|
|
102
|
+
if server == "granian":
|
|
103
|
+
_run_granian(app_target, config)
|
|
104
|
+
elif server == "uvicorn":
|
|
105
|
+
_run_uvicorn(app_target, config)
|
|
106
|
+
elif server == "gunicorn":
|
|
107
|
+
_run_gunicorn(app_target, config)
|
|
108
|
+
else:
|
|
109
|
+
raise ValueError(f"不支持的服务器类型: {server}")
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _run_granian(app_target: str, config: Dict[str, Any]):
|
|
113
|
+
"""使用 Granian 启动(内部函数)"""
|
|
114
|
+
try:
|
|
115
|
+
from granian import Granian
|
|
116
|
+
from granian.constants import Interfaces, Loops
|
|
117
|
+
except ImportError:
|
|
118
|
+
raise ImportError(
|
|
119
|
+
"Granian 未安装。请运行: pip install granian\n"
|
|
120
|
+
"或安装完整 web 依赖: pip install infomankit[web]"
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
print(f"🚀 使用 Granian 启动 [{config['app_name']}]")
|
|
124
|
+
print(f" 应用: {app_target}")
|
|
125
|
+
print(f" 环境: {config['env']}")
|
|
126
|
+
print(f" 地址: http://{config['host']}:{config['port']}")
|
|
127
|
+
print(f" 文档: http://{config['host']}:{config['port']}{config['docs_url']}")
|
|
128
|
+
print(f" 进程: {config['workers']} workers")
|
|
129
|
+
# 创建 Granian 实例(仅使用核心兼容参数)
|
|
130
|
+
# Granian 2.6.0+ 的核心参数
|
|
131
|
+
app = Granian(
|
|
132
|
+
target=app_target,
|
|
133
|
+
address=config["host"],
|
|
134
|
+
port=int(config["port"]),
|
|
135
|
+
interface=Interfaces.ASGI,
|
|
136
|
+
workers=config["workers"],
|
|
137
|
+
loop=Loops.auto,
|
|
138
|
+
log_level=config["log_level"],
|
|
139
|
+
reload=config["reload"],
|
|
140
|
+
)
|
|
141
|
+
app.serve()
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _run_uvicorn(app_target: str, config: Dict[str, Any]):
|
|
145
|
+
"""使用 Uvicorn 启动(内部函数)"""
|
|
146
|
+
try:
|
|
147
|
+
import uvicorn
|
|
148
|
+
except ImportError:
|
|
149
|
+
raise ImportError(
|
|
150
|
+
"Uvicorn 未安装。请运行: pip install uvicorn\n"
|
|
151
|
+
"或安装完整 web 依赖: pip install infomankit[web]"
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
print(f"🚀 使用 Uvicorn 启动 [{config['app_name']}]")
|
|
155
|
+
print(f" 应用: {app_target}")
|
|
156
|
+
print(f" 环境: {config['env']}")
|
|
157
|
+
print(f" 地址: http://{config['host']}:{config['port']}")
|
|
158
|
+
uvicorn.run(
|
|
159
|
+
app_target,
|
|
160
|
+
host=config["host"],
|
|
161
|
+
port=int(config["port"]),
|
|
162
|
+
reload=config["reload"],
|
|
163
|
+
log_level=config["log_level"],
|
|
164
|
+
access_log=config.get("access_log", config["reload"]),
|
|
165
|
+
workers=config["workers"] if not config["reload"] else 1, # reload 模式只能单进程
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
def _run_gunicorn(app_target: str, config: Dict[str, Any]):
|
|
170
|
+
"""使用 Gunicorn 启动(内部函数)"""
|
|
171
|
+
try:
|
|
172
|
+
import gunicorn
|
|
173
|
+
except ImportError:
|
|
174
|
+
raise ImportError(
|
|
175
|
+
"Gunicorn 未安装。请运行: pip install gunicorn\n"
|
|
176
|
+
"注意: Gunicorn 仅支持 Linux/macOS"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
print(f"🚀 使用 Gunicorn 启动 [{config['app_name']}]")
|
|
180
|
+
print(f" 应用: {app_target}")
|
|
181
|
+
print(f" 环境: {config['env']}")
|
|
182
|
+
print(f" 地址: http://{config['host']}:{config['port']}")
|
|
183
|
+
|
|
184
|
+
# Gunicorn 配置
|
|
185
|
+
bind_address = f"{config['host']}:{config['port']}"
|
|
186
|
+
worker_class = "uvicorn.workers.UvicornWorker"
|
|
187
|
+
workers = config["workers"]
|
|
188
|
+
os.system(
|
|
189
|
+
f'gunicorn {app_target} '
|
|
190
|
+
f'-b {bind_address} '
|
|
191
|
+
f'-w {workers} '
|
|
192
|
+
f'-k {worker_class} '
|
|
193
|
+
f'--log-level {config["log_level"]} '
|
|
194
|
+
f'--access-logfile - '
|
|
195
|
+
f'--error-logfile -'
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def main():
|
|
200
|
+
"""命令行入口"""
|
|
201
|
+
import argparse
|
|
202
|
+
|
|
203
|
+
parser = argparse.ArgumentParser(
|
|
204
|
+
description="Infoman Service Launcher - 启动 ASGI 应用服务器",
|
|
205
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
206
|
+
epilog="""
|
|
207
|
+
示例:
|
|
208
|
+
# 启动内置应用
|
|
209
|
+
python -m infoman.service.launch
|
|
210
|
+
|
|
211
|
+
# 启动自定义应用
|
|
212
|
+
python -m infoman.service.launch --app myapp.main:app
|
|
213
|
+
|
|
214
|
+
# 生产环境配置
|
|
215
|
+
python -m infoman.service.launch --server granian --workers 4 --port 8080
|
|
216
|
+
|
|
217
|
+
# 开发环境热重载
|
|
218
|
+
python -m infoman.service.launch --server uvicorn --reload
|
|
219
|
+
"""
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
parser.add_argument(
|
|
223
|
+
"--app",
|
|
224
|
+
default="infoman.service.app:application",
|
|
225
|
+
help="应用目标 (格式: module.path:app_instance, 默认: infoman.service.app:application)",
|
|
226
|
+
)
|
|
227
|
+
parser.add_argument(
|
|
228
|
+
"--server",
|
|
229
|
+
choices=["granian", "uvicorn", "gunicorn"],
|
|
230
|
+
default="granian",
|
|
231
|
+
help="选择 ASGI 服务器 (默认: granian)",
|
|
232
|
+
)
|
|
233
|
+
parser.add_argument(
|
|
234
|
+
"--host",
|
|
235
|
+
default=None,
|
|
236
|
+
help="监听地址 (默认: 从配置文件读取或 0.0.0.0)",
|
|
237
|
+
)
|
|
238
|
+
parser.add_argument(
|
|
239
|
+
"--port",
|
|
240
|
+
type=int,
|
|
241
|
+
default=None,
|
|
242
|
+
help="监听端口 (默认: 从配置文件读取或 8000)",
|
|
243
|
+
)
|
|
244
|
+
parser.add_argument(
|
|
245
|
+
"--workers",
|
|
246
|
+
type=int,
|
|
247
|
+
default=None,
|
|
248
|
+
help="工作进程数 (默认: 从配置文件读取或 2)",
|
|
249
|
+
)
|
|
250
|
+
parser.add_argument(
|
|
251
|
+
"--threads",
|
|
252
|
+
type=int,
|
|
253
|
+
default=None,
|
|
254
|
+
help="线程数 (仅 Granian, 默认: 1)",
|
|
255
|
+
)
|
|
256
|
+
parser.add_argument(
|
|
257
|
+
"--reload",
|
|
258
|
+
action="store_true",
|
|
259
|
+
help="启用热重载 (开发环境)",
|
|
260
|
+
)
|
|
261
|
+
parser.add_argument(
|
|
262
|
+
"--log-level",
|
|
263
|
+
choices=["debug", "info", "warning", "error", "critical"],
|
|
264
|
+
default=None,
|
|
265
|
+
help="日志级别 (默认: info)",
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
args = parser.parse_args()
|
|
269
|
+
|
|
270
|
+
# 调用 serve 函数
|
|
271
|
+
serve(
|
|
272
|
+
app_target=args.app,
|
|
273
|
+
server=args.server,
|
|
274
|
+
host=args.host,
|
|
275
|
+
port=args.port,
|
|
276
|
+
workers=args.workers,
|
|
277
|
+
threads=args.threads,
|
|
278
|
+
reload=args.reload,
|
|
279
|
+
log_level=args.log_level,
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
if __name__ == "__main__":
|
|
284
|
+
main()
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# -*- coding:utf-8 -*-
|
|
2
|
+
"""
|
|
3
|
+
# Time :2023/12/8 18:23
|
|
4
|
+
# Author :Maxwell
|
|
5
|
+
# version :python 3.9
|
|
6
|
+
# Description:
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import time
|
|
10
|
+
from fastapi import Request
|
|
11
|
+
from starlette.datastructures import MutableHeaders
|
|
12
|
+
from starlette.types import ASGIApp, Receive, Scope, Send, Message
|
|
13
|
+
from infoman.utils.hash.hash import HashManager
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class BaseMiddleware:
|
|
17
|
+
|
|
18
|
+
def __init__(
|
|
19
|
+
self,
|
|
20
|
+
app: ASGIApp,
|
|
21
|
+
) -> None:
|
|
22
|
+
self.app = app
|
|
23
|
+
|
|
24
|
+
async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:
|
|
25
|
+
if scope["type"] != "http":
|
|
26
|
+
await self.app(scope, receive, send)
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
start_time = time.time()
|
|
30
|
+
req = Request(scope, receive, send)
|
|
31
|
+
if not req.session.get("session"):
|
|
32
|
+
req.session.setdefault("session", HashManager.uuid())
|
|
33
|
+
|
|
34
|
+
async def send_wrapper(message: Message) -> None:
|
|
35
|
+
process_time = time.time() - start_time
|
|
36
|
+
if message["type"] == "http.response.start":
|
|
37
|
+
headers = MutableHeaders(scope=message)
|
|
38
|
+
headers.append("X-Process-Time", str(process_time))
|
|
39
|
+
await send(message)
|
|
40
|
+
|
|
41
|
+
await self.app(scope, receive, send_wrapper)
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# !/usr/bin/env python
|
|
2
|
+
# -*-coding:utf-8 -*-
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
# Time :2024/1/12 10:27
|
|
6
|
+
# Author :Maxwell
|
|
7
|
+
# Description:
|
|
8
|
+
"""
|
|
9
|
+
import time
|
|
10
|
+
import traceback
|
|
11
|
+
from fastapi import Request, Response
|
|
12
|
+
from starlette.middleware.base import BaseHTTPMiddleware
|
|
13
|
+
from infoman.logger import logger
|
|
14
|
+
from infoman.utils.http.info import ClientInfoExtractor
|
|
15
|
+
from starlette.responses import StreamingResponse, FileResponse
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class LoggingMiddleware(BaseHTTPMiddleware):
|
|
19
|
+
|
|
20
|
+
@staticmethod
|
|
21
|
+
def format_size(size_bytes: int) -> str:
|
|
22
|
+
if size_bytes < 1024:
|
|
23
|
+
return f"{size_bytes}B"
|
|
24
|
+
elif size_bytes < 1024 * 1024:
|
|
25
|
+
return f"{size_bytes / 1024:.2f}KB"
|
|
26
|
+
else:
|
|
27
|
+
return f"{size_bytes / (1024 * 1024):.2f}MB"
|
|
28
|
+
|
|
29
|
+
async def dispatch(self, request: Request, call_next) -> Response:
|
|
30
|
+
start_time = time.monotonic()
|
|
31
|
+
client_ip = ClientInfoExtractor.ip_address(request=request)
|
|
32
|
+
path = request.url.path[:200]
|
|
33
|
+
|
|
34
|
+
response = await call_next(request)
|
|
35
|
+
elapsed_ms = int((time.monotonic() - start_time) * 1000)
|
|
36
|
+
|
|
37
|
+
content_length = response.headers.get("content-length")
|
|
38
|
+
if content_length:
|
|
39
|
+
response_size = int(content_length)
|
|
40
|
+
size_str = self.format_size(response_size)
|
|
41
|
+
else:
|
|
42
|
+
size_str = "unknown"
|
|
43
|
+
|
|
44
|
+
logger.info(
|
|
45
|
+
f"Req: ip={client_ip}, elapsed_ms={elapsed_ms}, "
|
|
46
|
+
f"path={path}, status={response.status_code}, "
|
|
47
|
+
f"size={size_str}"
|
|
48
|
+
)
|
|
49
|
+
return response
|
|
50
|
+
|
|
51
|
+
|