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.
Files changed (143) hide show
  1. infoman/__init__.py +1 -0
  2. infoman/cli/README.md +378 -0
  3. infoman/cli/__init__.py +7 -0
  4. infoman/cli/commands/__init__.py +3 -0
  5. infoman/cli/commands/init.py +312 -0
  6. infoman/cli/scaffold.py +634 -0
  7. infoman/cli/templates/Makefile.template +132 -0
  8. infoman/cli/templates/app/__init__.py.template +3 -0
  9. infoman/cli/templates/app/app.py.template +4 -0
  10. infoman/cli/templates/app/models_base.py.template +18 -0
  11. infoman/cli/templates/app/models_entity_init.py.template +11 -0
  12. infoman/cli/templates/app/models_schemas_init.py.template +11 -0
  13. infoman/cli/templates/app/repository_init.py.template +11 -0
  14. infoman/cli/templates/app/routers_init.py.template +15 -0
  15. infoman/cli/templates/app/services_init.py.template +11 -0
  16. infoman/cli/templates/app/static_index.html.template +39 -0
  17. infoman/cli/templates/app/static_main.js.template +31 -0
  18. infoman/cli/templates/app/static_style.css.template +111 -0
  19. infoman/cli/templates/app/utils_init.py.template +11 -0
  20. infoman/cli/templates/config/.env.dev.template +43 -0
  21. infoman/cli/templates/config/.env.prod.template +43 -0
  22. infoman/cli/templates/config/README.md.template +28 -0
  23. infoman/cli/templates/docker/.dockerignore.template +60 -0
  24. infoman/cli/templates/docker/Dockerfile.template +47 -0
  25. infoman/cli/templates/docker/README.md.template +240 -0
  26. infoman/cli/templates/docker/docker-compose.yml.template +81 -0
  27. infoman/cli/templates/docker/mysql_custom.cnf.template +42 -0
  28. infoman/cli/templates/docker/mysql_init.sql.template +15 -0
  29. infoman/cli/templates/project/.env.example.template +1 -0
  30. infoman/cli/templates/project/.gitignore.template +60 -0
  31. infoman/cli/templates/project/Makefile.template +38 -0
  32. infoman/cli/templates/project/README.md.template +137 -0
  33. infoman/cli/templates/project/deploy.sh.template +97 -0
  34. infoman/cli/templates/project/main.py.template +10 -0
  35. infoman/cli/templates/project/manage.sh.template +97 -0
  36. infoman/cli/templates/project/pyproject.toml.template +47 -0
  37. infoman/cli/templates/project/service.sh.template +203 -0
  38. infoman/config/__init__.py +25 -0
  39. infoman/config/base.py +67 -0
  40. infoman/config/db_cache.py +237 -0
  41. infoman/config/db_relation.py +181 -0
  42. infoman/config/db_vector.py +39 -0
  43. infoman/config/jwt.py +16 -0
  44. infoman/config/llm.py +16 -0
  45. infoman/config/log.py +627 -0
  46. infoman/config/mq.py +26 -0
  47. infoman/config/settings.py +65 -0
  48. infoman/llm/__init__.py +0 -0
  49. infoman/llm/llm.py +297 -0
  50. infoman/logger/__init__.py +57 -0
  51. infoman/logger/context.py +191 -0
  52. infoman/logger/core.py +358 -0
  53. infoman/logger/filters.py +157 -0
  54. infoman/logger/formatters.py +138 -0
  55. infoman/logger/handlers.py +276 -0
  56. infoman/logger/metrics.py +160 -0
  57. infoman/performance/README.md +583 -0
  58. infoman/performance/__init__.py +19 -0
  59. infoman/performance/cli.py +215 -0
  60. infoman/performance/config.py +166 -0
  61. infoman/performance/reporter.py +519 -0
  62. infoman/performance/runner.py +303 -0
  63. infoman/performance/standards.py +222 -0
  64. infoman/service/__init__.py +8 -0
  65. infoman/service/app.py +67 -0
  66. infoman/service/core/__init__.py +0 -0
  67. infoman/service/core/auth.py +105 -0
  68. infoman/service/core/lifespan.py +132 -0
  69. infoman/service/core/monitor.py +57 -0
  70. infoman/service/core/response.py +37 -0
  71. infoman/service/exception/__init__.py +7 -0
  72. infoman/service/exception/error.py +274 -0
  73. infoman/service/exception/exception.py +25 -0
  74. infoman/service/exception/handler.py +238 -0
  75. infoman/service/infrastructure/__init__.py +8 -0
  76. infoman/service/infrastructure/base.py +212 -0
  77. infoman/service/infrastructure/db_cache/__init__.py +8 -0
  78. infoman/service/infrastructure/db_cache/manager.py +194 -0
  79. infoman/service/infrastructure/db_relation/__init__.py +41 -0
  80. infoman/service/infrastructure/db_relation/manager.py +300 -0
  81. infoman/service/infrastructure/db_relation/manager_pro.py +408 -0
  82. infoman/service/infrastructure/db_relation/mysql.py +52 -0
  83. infoman/service/infrastructure/db_relation/pgsql.py +54 -0
  84. infoman/service/infrastructure/db_relation/sqllite.py +25 -0
  85. infoman/service/infrastructure/db_vector/__init__.py +40 -0
  86. infoman/service/infrastructure/db_vector/manager.py +201 -0
  87. infoman/service/infrastructure/db_vector/qdrant.py +322 -0
  88. infoman/service/infrastructure/mq/__init__.py +15 -0
  89. infoman/service/infrastructure/mq/manager.py +178 -0
  90. infoman/service/infrastructure/mq/nats/__init__.py +0 -0
  91. infoman/service/infrastructure/mq/nats/nats_client.py +57 -0
  92. infoman/service/infrastructure/mq/nats/nats_event_router.py +25 -0
  93. infoman/service/launch.py +284 -0
  94. infoman/service/middleware/__init__.py +7 -0
  95. infoman/service/middleware/base.py +41 -0
  96. infoman/service/middleware/logging.py +51 -0
  97. infoman/service/middleware/rate_limit.py +301 -0
  98. infoman/service/middleware/request_id.py +21 -0
  99. infoman/service/middleware/white_list.py +24 -0
  100. infoman/service/models/__init__.py +8 -0
  101. infoman/service/models/base.py +441 -0
  102. infoman/service/models/type/embed.py +70 -0
  103. infoman/service/routers/__init__.py +18 -0
  104. infoman/service/routers/health_router.py +311 -0
  105. infoman/service/routers/monitor_router.py +44 -0
  106. infoman/service/utils/__init__.py +8 -0
  107. infoman/service/utils/cache/__init__.py +0 -0
  108. infoman/service/utils/cache/cache.py +192 -0
  109. infoman/service/utils/module_loader.py +10 -0
  110. infoman/service/utils/parse.py +10 -0
  111. infoman/service/utils/resolver/__init__.py +8 -0
  112. infoman/service/utils/resolver/base.py +47 -0
  113. infoman/service/utils/resolver/resp.py +102 -0
  114. infoman/service/vector/__init__.py +20 -0
  115. infoman/service/vector/base.py +56 -0
  116. infoman/service/vector/qdrant.py +125 -0
  117. infoman/service/vector/service.py +67 -0
  118. infoman/utils/__init__.py +2 -0
  119. infoman/utils/decorators/__init__.py +8 -0
  120. infoman/utils/decorators/cache.py +137 -0
  121. infoman/utils/decorators/retry.py +99 -0
  122. infoman/utils/decorators/safe_execute.py +99 -0
  123. infoman/utils/decorators/timing.py +99 -0
  124. infoman/utils/encryption/__init__.py +8 -0
  125. infoman/utils/encryption/aes.py +66 -0
  126. infoman/utils/encryption/ecc.py +108 -0
  127. infoman/utils/encryption/rsa.py +112 -0
  128. infoman/utils/file/__init__.py +0 -0
  129. infoman/utils/file/handler.py +22 -0
  130. infoman/utils/hash/__init__.py +0 -0
  131. infoman/utils/hash/hash.py +61 -0
  132. infoman/utils/http/__init__.py +8 -0
  133. infoman/utils/http/client.py +62 -0
  134. infoman/utils/http/info.py +94 -0
  135. infoman/utils/http/result.py +19 -0
  136. infoman/utils/notification/__init__.py +8 -0
  137. infoman/utils/notification/feishu.py +35 -0
  138. infoman/utils/text/__init__.py +8 -0
  139. infoman/utils/text/extractor.py +111 -0
  140. infomankit-0.3.23.dist-info/METADATA +632 -0
  141. infomankit-0.3.23.dist-info/RECORD +143 -0
  142. infomankit-0.3.23.dist-info/WHEEL +4 -0
  143. infomankit-0.3.23.dist-info/entry_points.txt +5 -0
@@ -0,0 +1,301 @@
1
+ import re
2
+ import time
3
+ import asyncio
4
+ from enum import Enum
5
+ from fastapi import Request, Response
6
+ from fastapi.responses import JSONResponse
7
+ from starlette.middleware.base import BaseHTTPMiddleware
8
+ from typing import Dict, List, Optional, Callable, Union
9
+ from infoman.logger import logger
10
+
11
+
12
+ class LimitStrategy(str, Enum):
13
+ IP = "ip"
14
+ PATH = "path"
15
+ USER = "user"
16
+ IP_PATH = "ip_path"
17
+ USER_PATH = "user_path"
18
+ GLOBAL = "global"
19
+
20
+
21
+ class RateLimitExceeded(Exception):
22
+
23
+ def __init__(self, limit: int, window: int, retry_after: int):
24
+ self.limit = limit
25
+ self.window = window
26
+ self.retry_after = retry_after
27
+ message = f"Rate limit exceeded: {limit} requests per {window} seconds. Try again in {retry_after} seconds."
28
+ super().__init__(message)
29
+
30
+
31
+ class RateLimitMiddleware(BaseHTTPMiddleware):
32
+
33
+ def __init__(
34
+ self,
35
+ app,
36
+ max_requests: int = 100,
37
+ window: int = 60,
38
+ strategy: Union[LimitStrategy, str] = LimitStrategy.IP,
39
+ whitelist: List[str] = None,
40
+ blacklist: List[str] = None,
41
+ user_identifier: Callable[[Request], str] = None,
42
+ custom_response: Callable[[int], JSONResponse] = None,
43
+ path_pattern: Optional[str] = None,
44
+ exclude_paths: List[str] = None,
45
+ clean_interval: int = 60,
46
+ enable_statistics: bool = False,
47
+ redis_url: Optional[str] = None,
48
+ ):
49
+ """
50
+ 初始化限流中间件
51
+
52
+ Args:
53
+ app: FastAPI应用
54
+ max_requests: 时间窗口内允许的最大请求数
55
+ window: 时间窗口大小(秒)
56
+ strategy: 限流策略,可以是LimitStrategy枚举或字符串
57
+ whitelist: IP白名单列表,这些IP不受限流影响
58
+ blacklist: IP黑名单列表,这些IP总是被拒绝
59
+ user_identifier: 从请求中提取用户标识的函数
60
+ custom_response: 自定义限流响应的函数
61
+ path_pattern: 只对匹配此正则表达式的路径应用限流
62
+ exclude_paths: 不进行限流的路径列表
63
+ clean_interval: 清理过期记录的间隔(秒)
64
+ enable_statistics: 是否启用详细统计
65
+ redis_url: Redis连接URL,如果提供则使用Redis存储请求记录
66
+ """
67
+ super().__init__(app)
68
+ self.max_requests = max_requests
69
+ self.window = window
70
+ self.strategy = (
71
+ strategy if isinstance(strategy, LimitStrategy) else LimitStrategy(strategy)
72
+ )
73
+ self.whitelist = set(whitelist or [])
74
+ self.blacklist = set(blacklist or [])
75
+ self.user_identifier = user_identifier
76
+ self.custom_response = custom_response
77
+ self.path_pattern = re.compile(path_pattern) if path_pattern else None
78
+ self.exclude_paths = set(exclude_paths or [])
79
+ self.clean_interval = clean_interval
80
+ self.enable_statistics = enable_statistics
81
+ self.redis_url = redis_url
82
+
83
+ self.requests: Dict[str, List[float]] = {}
84
+ self.statistics = {
85
+ "total_requests": 0,
86
+ "limited_requests": 0,
87
+ "last_reset": time.time(),
88
+ "by_ip": {},
89
+ "by_path": {},
90
+ }
91
+
92
+ self.redis = None
93
+ if self.redis_url:
94
+ self._setup_redis()
95
+ self._setup_cleanup_task()
96
+
97
+ def _setup_redis(self):
98
+ try:
99
+ import redis.asyncio as redis
100
+
101
+ self.redis = redis.from_url(self.redis_url)
102
+ logger.info(f"Using Redis for rate limiting: {self.redis_url}")
103
+ except ImportError:
104
+ logger.warning(
105
+ "redis-py package not installed. Falling back to in-memory storage."
106
+ )
107
+ self.redis = None
108
+
109
+ def _setup_cleanup_task(self):
110
+
111
+ async def cleanup_task():
112
+ while True:
113
+ await asyncio.sleep(self.clean_interval)
114
+ self._cleanup_expired_records()
115
+
116
+ if (
117
+ self.enable_statistics
118
+ and time.time() - self.statistics["last_reset"] > 3600
119
+ ):
120
+ self.statistics = {
121
+ "total_requests": 0,
122
+ "limited_requests": 0,
123
+ "last_reset": time.time(),
124
+ "by_ip": {},
125
+ "by_path": {},
126
+ }
127
+
128
+ asyncio.create_task(cleanup_task())
129
+
130
+ def _cleanup_expired_records(self):
131
+ current_time = time.time()
132
+ expired_keys = []
133
+
134
+ for key, timestamps in self.requests.items():
135
+ valid_timestamps = [
136
+ ts for ts in timestamps if ts > current_time - self.window
137
+ ]
138
+ if not valid_timestamps:
139
+ expired_keys.append(key)
140
+ else:
141
+ self.requests[key] = valid_timestamps
142
+
143
+ for key in expired_keys:
144
+ del self.requests[key]
145
+ logger.debug(
146
+ f"Cleaned up {len(expired_keys)} expired records. Current records: {len(self.requests)}"
147
+ )
148
+
149
+ def _get_request_key(self, request: Request) -> str:
150
+ client_ip = request.client.host
151
+ path = request.url.path
152
+
153
+ if self.strategy == LimitStrategy.IP:
154
+ return f"ip:{client_ip}"
155
+ elif self.strategy == LimitStrategy.PATH:
156
+ return f"path:{path}"
157
+ elif self.strategy == LimitStrategy.USER:
158
+ user_id = (
159
+ self.user_identifier(request) if self.user_identifier else "anonymous"
160
+ )
161
+ return f"user:{user_id}"
162
+ elif self.strategy == LimitStrategy.IP_PATH:
163
+ return f"ip:{client_ip}:path:{path}"
164
+ elif self.strategy == LimitStrategy.USER_PATH:
165
+ user_id = (
166
+ self.user_identifier(request) if self.user_identifier else "anonymous"
167
+ )
168
+ return f"user:{user_id}:path:{path}"
169
+ elif self.strategy == LimitStrategy.GLOBAL:
170
+ return "global"
171
+ else:
172
+ return f"ip:{client_ip}"
173
+
174
+ async def _get_request_count(self, key: str, current_time: float) -> int:
175
+ if self.redis:
176
+ try:
177
+ timestamps = await self.redis.zrangebyscore(
178
+ key, current_time - self.window, current_time
179
+ )
180
+ return len(timestamps)
181
+ except Exception as e:
182
+ logger.error(f"Redis error: {e}")
183
+ if key not in self.requests:
184
+ self.requests[key] = []
185
+ self.requests[key] = [
186
+ ts for ts in self.requests[key] if ts > current_time - self.window
187
+ ]
188
+ return len(self.requests[key])
189
+ else:
190
+ if key not in self.requests:
191
+ self.requests[key] = []
192
+ self.requests[key] = [
193
+ ts for ts in self.requests[key] if ts > current_time - self.window
194
+ ]
195
+ return len(self.requests[key])
196
+
197
+ async def _add_request_record(self, key: str, current_time: float) -> None:
198
+ if self.redis:
199
+ try:
200
+ await self.redis.zadd(key, {str(current_time): current_time})
201
+ await self.redis.expire(key, self.window * 2)
202
+ except Exception as e:
203
+ logger.error(f"Redis error: {e}")
204
+ if key not in self.requests:
205
+ self.requests[key] = []
206
+ self.requests[key].append(current_time)
207
+ else:
208
+ if key not in self.requests:
209
+ self.requests[key] = []
210
+ self.requests[key].append(current_time)
211
+
212
+ def _update_statistics(self, request: Request, limited: bool) -> None:
213
+ if not self.enable_statistics:
214
+ return
215
+
216
+ self.statistics["total_requests"] += 1
217
+ if limited:
218
+ self.statistics["limited_requests"] += 1
219
+
220
+ client_ip = request.client.host
221
+ if client_ip not in self.statistics["by_ip"]:
222
+ self.statistics["by_ip"][client_ip] = {"total": 0, "limited": 0}
223
+ self.statistics["by_ip"][client_ip]["total"] += 1
224
+ if limited:
225
+ self.statistics["by_ip"][client_ip]["limited"] += 1
226
+
227
+ path = request.url.path
228
+ if path not in self.statistics["by_path"]:
229
+ self.statistics["by_path"][path] = {"total": 0, "limited": 0}
230
+ self.statistics["by_path"][path]["total"] += 1
231
+ if limited:
232
+ self.statistics["by_path"][path]["limited"] += 1
233
+
234
+ def _create_rate_limit_response(self, retry_after: int) -> JSONResponse:
235
+ if self.custom_response:
236
+ return self.custom_response(retry_after)
237
+
238
+ return JSONResponse(
239
+ status_code=429,
240
+ content={
241
+ "error": "Too Many Requests",
242
+ "detail": f"Rate limit exceeded: {self.max_requests} requests per {self.window} seconds.",
243
+ "retry_after": retry_after,
244
+ },
245
+ headers={"Retry-After": str(retry_after)},
246
+ )
247
+
248
+ def _should_limit_path(self, path: str) -> bool:
249
+ if path in self.exclude_paths:
250
+ return False
251
+
252
+ if self.path_pattern and not self.path_pattern.match(path):
253
+ return False
254
+
255
+ return True
256
+
257
+ async def dispatch(self, request: Request, call_next) -> Response:
258
+ if not self._should_limit_path(request.url.path):
259
+ return await call_next(request)
260
+
261
+ client_ip = request.client.host
262
+ if client_ip in self.whitelist:
263
+ return await call_next(request)
264
+ if client_ip in self.blacklist:
265
+ return JSONResponse(
266
+ status_code=403,
267
+ content={
268
+ "error": "Forbidden",
269
+ "detail": "Your IP address is blacklisted.",
270
+ },
271
+ )
272
+
273
+ key = self._get_request_key(request)
274
+ current_time = int(time.monotonic() * 1000)
275
+ request_count = await self._get_request_count(key, current_time)
276
+
277
+ if request_count >= self.max_requests:
278
+ oldest_timestamp = (
279
+ min(self.requests.get(key, [current_time]))
280
+ if not self.redis
281
+ else current_time - self.window
282
+ )
283
+ retry_after = max(1, int(self.window - (current_time - oldest_timestamp)))
284
+ self._update_statistics(request, limited=True)
285
+ logger.warning(
286
+ f"Rate limit exceeded for {key}. "
287
+ f"Count: {request_count}/{self.max_requests}, "
288
+ f"Window: {self.window}s, "
289
+ f"Retry-After: {retry_after}s"
290
+ )
291
+ return self._create_rate_limit_response(retry_after)
292
+
293
+ await self._add_request_record(key, current_time)
294
+
295
+ self._update_statistics(request, limited=False)
296
+ response = await call_next(request)
297
+ remaining = self.max_requests - request_count - 1
298
+ response.headers["X-RateLimit-Limit"] = str(self.max_requests)
299
+ response.headers["X-RateLimit-Remaining"] = str(max(0, remaining))
300
+ response.headers["X-RateLimit-Reset"] = str(int(current_time + self.window))
301
+ return response
@@ -0,0 +1,21 @@
1
+ # !/usr/bin/env python
2
+ # -*-coding:utf-8 -*-
3
+
4
+ """
5
+ # Time :2025/6/21 21:21
6
+ # Author :Maxwell
7
+ # Description:
8
+ """
9
+ from fastapi import Request, Response
10
+ from starlette.middleware.base import BaseHTTPMiddleware
11
+ from typing import Callable
12
+ from infoman.utils.hash.hash import HashManager
13
+
14
+
15
+ class RequestIDMiddleware(BaseHTTPMiddleware):
16
+ async def dispatch(self, request: Request, call_next: Callable) -> Response:
17
+ request_id = HashManager.time_hash()
18
+ request.state.request_id = request_id
19
+ response = await call_next(request)
20
+ response.headers["X-Request-ID"] = request_id
21
+ return response
@@ -0,0 +1,24 @@
1
+ # !/usr/bin/env python
2
+ # -*-coding:utf-8 -*-
3
+
4
+ """
5
+ # Time :2025/6/18 14:45
6
+ # Author :Maxwell
7
+ # Description:
8
+ """
9
+
10
+ from fastapi import Request
11
+ from starlette.middleware.base import BaseHTTPMiddleware
12
+ from fastapi.responses import PlainTextResponse
13
+
14
+
15
+ class IPWhitelistMiddleware(BaseHTTPMiddleware):
16
+ def __init__(self, app, whitelist):
17
+ super().__init__(app)
18
+ self.whitelist = whitelist
19
+
20
+ async def dispatch(self, request: Request, call_next):
21
+ client_ip = request.client.host
22
+ if client_ip not in self.whitelist:
23
+ return PlainTextResponse(status_code=403, content="IP not allowed")
24
+ return await call_next(request)
@@ -0,0 +1,8 @@
1
+ # !/usr/bin/env python
2
+ # -*-coding:utf-8 -*-
3
+
4
+ """
5
+ # Time :2025/6/26 15:41
6
+ # Author :Maxwell
7
+ # Description:
8
+ """