fastapi-augment 0.1.0__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.
- fastapi_augment/__init__.py +24 -0
- fastapi_augment/common/__init__.py +61 -0
- fastapi_augment/common/constants.py +26 -0
- fastapi_augment/common/exception_handlers.py +178 -0
- fastapi_augment/common/exceptions.py +162 -0
- fastapi_augment/common/utils/__init__.py +5 -0
- fastapi_augment/common/utils/strings.py +175 -0
- fastapi_augment/config/__init__.py +8 -0
- fastapi_augment/config/settings.py +104 -0
- fastapi_augment/db/__init__.py +5 -0
- fastapi_augment/db/sqlalchemy/__init__.py +20 -0
- fastapi_augment/db/sqlalchemy/alembic/__init__.py +5 -0
- fastapi_augment/db/sqlalchemy/alembic/env.py +141 -0
- fastapi_augment/db/sqlalchemy/base.py +9 -0
- fastapi_augment/db/sqlalchemy/crud_base.py +426 -0
- fastapi_augment/db/sqlalchemy/engine.py +238 -0
- fastapi_augment/db/sqlalchemy/migrate.py +356 -0
- fastapi_augment/db/sqlalchemy/mixins/__init__.py +18 -0
- fastapi_augment/db/sqlalchemy/mixins/audit.py +61 -0
- fastapi_augment/db/sqlalchemy/mixins/soft_delete.py +80 -0
- fastapi_augment/db/sqlalchemy/mixins/timestamp.py +48 -0
- fastapi_augment/db/sqlalchemy/model_base.py +47 -0
- fastapi_augment/db/sqlalchemy/session.py +160 -0
- fastapi_augment/factory.py +238 -0
- fastapi_augment/health/__init__.py +34 -0
- fastapi_augment/health/checker.py +101 -0
- fastapi_augment/health/checkers.py +109 -0
- fastapi_augment/health/router.py +87 -0
- fastapi_augment/lifespan.py +450 -0
- fastapi_augment/log/__init__.py +26 -0
- fastapi_augment/log/config.py +201 -0
- fastapi_augment/log/factory.py +32 -0
- fastapi_augment/log/filters.py +23 -0
- fastapi_augment/log/handlers.py +81 -0
- fastapi_augment/middlewares/__init__.py +20 -0
- fastapi_augment/middlewares/base.py +79 -0
- fastapi_augment/middlewares/request_id.py +82 -0
- fastapi_augment/openapi.py +110 -0
- fastapi_augment/py.typed +0 -0
- fastapi_augment/schemas/__init__.py +29 -0
- fastapi_augment/schemas/base.py +32 -0
- fastapi_augment/schemas/pagination.py +46 -0
- fastapi_augment/schemas/request.py +28 -0
- fastapi_augment/schemas/response.py +139 -0
- fastapi_augment/schemas/types.py +11 -0
- fastapi_augment-0.1.0.dist-info/METADATA +654 -0
- fastapi_augment-0.1.0.dist-info/RECORD +50 -0
- fastapi_augment-0.1.0.dist-info/WHEEL +5 -0
- fastapi_augment-0.1.0.dist-info/entry_points.txt +2 -0
- fastapi_augment-0.1.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@Author : zarkhan
|
|
3
|
+
@CreateDate : 2026/9/6
|
|
4
|
+
@Description: 健康检查路由工厂
|
|
5
|
+
- 提供 create_health_router() 创建可配置的健康检查路由
|
|
6
|
+
- 自动注册 AppChecker,可选注册 DatabaseChecker 和自定义检查器
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Sequence
|
|
11
|
+
|
|
12
|
+
from fastapi import APIRouter, Request, Response
|
|
13
|
+
|
|
14
|
+
from .checker import BaseChecker, HealthResponse, _worst_status, STATUS_HEALTHY, STATUS_UNHEALTHY
|
|
15
|
+
from .checkers import AppChecker, DatabaseChecker
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def create_health_router(
|
|
19
|
+
*,
|
|
20
|
+
path: str = '/health',
|
|
21
|
+
tags: list[str] | None = None,
|
|
22
|
+
include_db_check: bool = True,
|
|
23
|
+
extra_checkers: Sequence[BaseChecker] | None = None,
|
|
24
|
+
) -> APIRouter:
|
|
25
|
+
"""创建健康检查路由。
|
|
26
|
+
|
|
27
|
+
默认包含 ``AppChecker``(应用状态),可选 ``DatabaseChecker``(数据库连通性),
|
|
28
|
+
以及任意自定义检查器::
|
|
29
|
+
|
|
30
|
+
from fastapi_augment.health import create_health_router
|
|
31
|
+
|
|
32
|
+
# 最简用法:仅应用状态
|
|
33
|
+
app.include_router(create_health_router(include_db_check=False))
|
|
34
|
+
|
|
35
|
+
# 完整用法:应用 + 数据库 + 自定义检查器
|
|
36
|
+
app.include_router(create_health_router(
|
|
37
|
+
extra_checkers=[RedisChecker()],
|
|
38
|
+
))
|
|
39
|
+
|
|
40
|
+
Args:
|
|
41
|
+
path: 健康检查端点路径
|
|
42
|
+
tags: OpenAPI 标签
|
|
43
|
+
include_db_check: 是否包含数据库连通性检查(需要 app.state.engine_manager)
|
|
44
|
+
extra_checkers: 额外的自定义检查器列表
|
|
45
|
+
|
|
46
|
+
Returns:
|
|
47
|
+
配置好的健康检查 APIRouter
|
|
48
|
+
"""
|
|
49
|
+
router = APIRouter(tags=tags or ['Health'])
|
|
50
|
+
|
|
51
|
+
# 组装检查器列表
|
|
52
|
+
checkers: list[BaseChecker] = [AppChecker()]
|
|
53
|
+
if include_db_check:
|
|
54
|
+
checkers.append(DatabaseChecker())
|
|
55
|
+
if extra_checkers:
|
|
56
|
+
checkers.extend(extra_checkers)
|
|
57
|
+
|
|
58
|
+
@router.get(path, response_model=HealthResponse)
|
|
59
|
+
async def health_check(request: Request, response: Response) -> HealthResponse:
|
|
60
|
+
"""执行所有健康检查并返回聚合结果。
|
|
61
|
+
|
|
62
|
+
任一检查器返回 unhealthy 时,HTTP 状态码为 503。
|
|
63
|
+
"""
|
|
64
|
+
results = []
|
|
65
|
+
for checker in checkers:
|
|
66
|
+
try:
|
|
67
|
+
result = await checker.check(request.app)
|
|
68
|
+
except Exception as e:
|
|
69
|
+
# 检查器自身异常,兜底为 unhealthy
|
|
70
|
+
from .checker import CheckResult
|
|
71
|
+
result = CheckResult(
|
|
72
|
+
name=checker.name,
|
|
73
|
+
status=STATUS_UNHEALTHY,
|
|
74
|
+
details={'error': str(e)},
|
|
75
|
+
)
|
|
76
|
+
results.append(result)
|
|
77
|
+
|
|
78
|
+
# 总体状态取最差
|
|
79
|
+
overall = _worst_status(*(r.status for r in results)) if results else STATUS_HEALTHY
|
|
80
|
+
|
|
81
|
+
# 不健康时返回 503
|
|
82
|
+
if overall == STATUS_UNHEALTHY:
|
|
83
|
+
response.status_code = 503
|
|
84
|
+
|
|
85
|
+
return HealthResponse(status=overall, checks=results)
|
|
86
|
+
|
|
87
|
+
return router
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@Author : hangu
|
|
3
|
+
@CreateDate : 2026/9/3
|
|
4
|
+
@Description : 应用生命周期钩子管理 (完整版)
|
|
5
|
+
- core_registry 始终在首位执行(基础/核心钩子)
|
|
6
|
+
- 支持多个自定义注册表,通过 app.state.registries 注入
|
|
7
|
+
- 每个注册表独立管理优先级、超时、异常策略
|
|
8
|
+
"""
|
|
9
|
+
import asyncio
|
|
10
|
+
from contextlib import asynccontextmanager
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from inspect import signature, iscoroutinefunction
|
|
13
|
+
from logging import getLogger, Logger
|
|
14
|
+
from typing import (
|
|
15
|
+
runtime_checkable,
|
|
16
|
+
Protocol,
|
|
17
|
+
overload,
|
|
18
|
+
Callable,
|
|
19
|
+
AsyncGenerator,
|
|
20
|
+
Sequence,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
from fastapi import FastAPI
|
|
24
|
+
|
|
25
|
+
_logger = getLogger(__name__)
|
|
26
|
+
|
|
27
|
+
# -------------------------- 常量定义 --------------------------
|
|
28
|
+
DEFAULT_PRIORITY: int = 50
|
|
29
|
+
STARTUP_ABORT_ON_EXCEPTION: bool = True
|
|
30
|
+
SHUTDOWN_ABORT_ON_EXCEPTION: bool = False
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
# ---------- 类型定义 ----------
|
|
34
|
+
@runtime_checkable
|
|
35
|
+
class _NoArgHook(Protocol):
|
|
36
|
+
"""无参数钩子协议,用于类型检查"""
|
|
37
|
+
|
|
38
|
+
async def __call__(self) -> None: ...
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
@runtime_checkable
|
|
42
|
+
class _AppArgHook(Protocol):
|
|
43
|
+
"""带 FastAPI 应用实例参数的钩子协议"""
|
|
44
|
+
|
|
45
|
+
async def __call__(self, app: FastAPI) -> None: ...
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
HookFunc = _NoArgHook | _AppArgHook
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True, slots=True)
|
|
52
|
+
class _HookItem:
|
|
53
|
+
"""钩子条目,存储单个钩子的元数据"""
|
|
54
|
+
func: HookFunc
|
|
55
|
+
priority: int
|
|
56
|
+
abort_on_exception: bool
|
|
57
|
+
needs_app: bool
|
|
58
|
+
timeout: int | float | None = None
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
# ---------- 注册表 ----------
|
|
62
|
+
class HookRegistry:
|
|
63
|
+
"""生命周期钩子注册表。
|
|
64
|
+
|
|
65
|
+
每个实例独立管理自己的钩子列表,支持:
|
|
66
|
+
- 注册启动/关闭钩子
|
|
67
|
+
- 优先级控制(启动降序,关闭升序)
|
|
68
|
+
- 超时控制
|
|
69
|
+
- 异常时是否终止执行
|
|
70
|
+
- 装饰器语法
|
|
71
|
+
- 去重(基于函数 id)
|
|
72
|
+
|
|
73
|
+
Attributes:
|
|
74
|
+
_startup_hooks (list): 启动钩子列表
|
|
75
|
+
_shutdown_hooks (list): 关闭钩子列表
|
|
76
|
+
_startup_seen (set): 已注册的启动钩子函数 id 集合
|
|
77
|
+
_shutdown_seen (set): 已注册的关闭钩子函数 id 集合
|
|
78
|
+
_logger: 实例日志器
|
|
79
|
+
"""
|
|
80
|
+
|
|
81
|
+
__slots__ = ('_startup_hooks', '_shutdown_hooks', '_startup_seen', '_shutdown_seen', '_logger')
|
|
82
|
+
|
|
83
|
+
def __init__(self, logger: Logger | None = None) -> None:
|
|
84
|
+
"""初始化一个新的注册表实例,所有钩子列表为空"""
|
|
85
|
+
self._startup_hooks: list[_HookItem] = []
|
|
86
|
+
self._shutdown_hooks: list[_HookItem] = []
|
|
87
|
+
self._startup_seen: set[int] = set()
|
|
88
|
+
self._shutdown_seen: set[int] = set()
|
|
89
|
+
self._logger = logger or _logger
|
|
90
|
+
|
|
91
|
+
def register_startup(
|
|
92
|
+
self,
|
|
93
|
+
func: HookFunc,
|
|
94
|
+
priority: int = DEFAULT_PRIORITY,
|
|
95
|
+
abort_on_exception: bool = STARTUP_ABORT_ON_EXCEPTION,
|
|
96
|
+
timeout: int | float | None = None,
|
|
97
|
+
) -> None:
|
|
98
|
+
"""注册启动钩子。
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
func: 钩子函数
|
|
102
|
+
priority: 优先级(越大越先执行)
|
|
103
|
+
abort_on_exception: 异常时是否终止启动过程
|
|
104
|
+
timeout: 超时秒数(None 表示不限制)
|
|
105
|
+
"""
|
|
106
|
+
self._register(
|
|
107
|
+
self._startup_hooks,
|
|
108
|
+
self._startup_seen,
|
|
109
|
+
func,
|
|
110
|
+
priority,
|
|
111
|
+
abort_on_exception,
|
|
112
|
+
timeout,
|
|
113
|
+
reverse_sort=True,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def register_shutdown(
|
|
117
|
+
self,
|
|
118
|
+
func: HookFunc,
|
|
119
|
+
priority: int = DEFAULT_PRIORITY,
|
|
120
|
+
abort_on_exception: bool = SHUTDOWN_ABORT_ON_EXCEPTION,
|
|
121
|
+
timeout: int | float | None = None,
|
|
122
|
+
) -> None:
|
|
123
|
+
"""注册关闭钩子。
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
func: 钩子函数
|
|
127
|
+
priority: 优先级(越大越先执行)
|
|
128
|
+
abort_on_exception: 异常时是否终止关闭过程
|
|
129
|
+
timeout: 超时秒数(None 表示不限制)
|
|
130
|
+
"""
|
|
131
|
+
self._register(
|
|
132
|
+
self._shutdown_hooks,
|
|
133
|
+
self._shutdown_seen,
|
|
134
|
+
func,
|
|
135
|
+
priority,
|
|
136
|
+
abort_on_exception,
|
|
137
|
+
timeout,
|
|
138
|
+
reverse_sort=False,
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
@overload
|
|
142
|
+
def on_startup(self, func: HookFunc, /) -> HookFunc: ...
|
|
143
|
+
|
|
144
|
+
@overload
|
|
145
|
+
def on_startup(
|
|
146
|
+
self,
|
|
147
|
+
*,
|
|
148
|
+
priority: int = DEFAULT_PRIORITY,
|
|
149
|
+
abort_on_exception: bool = STARTUP_ABORT_ON_EXCEPTION,
|
|
150
|
+
timeout: int | float | None = None,
|
|
151
|
+
) -> Callable[[HookFunc], HookFunc]: ...
|
|
152
|
+
|
|
153
|
+
def on_startup(
|
|
154
|
+
self,
|
|
155
|
+
func: HookFunc | None = None,
|
|
156
|
+
*,
|
|
157
|
+
priority: int = DEFAULT_PRIORITY,
|
|
158
|
+
abort_on_exception: bool = STARTUP_ABORT_ON_EXCEPTION,
|
|
159
|
+
timeout: int | float | None = None,
|
|
160
|
+
) -> Callable[[HookFunc], HookFunc] | HookFunc:
|
|
161
|
+
"""启动钩子装饰器。
|
|
162
|
+
|
|
163
|
+
可带参数调用:@registry.on_startup(priority=100)
|
|
164
|
+
或不带参数:@registry.on_startup
|
|
165
|
+
|
|
166
|
+
Args:
|
|
167
|
+
func: 被装饰的函数
|
|
168
|
+
priority: 优先级(越大越先执行)
|
|
169
|
+
abort_on_exception: 异常时是否终止
|
|
170
|
+
timeout: 超时秒数
|
|
171
|
+
|
|
172
|
+
Returns:
|
|
173
|
+
装饰器或原函数(取决于调用方式)
|
|
174
|
+
"""
|
|
175
|
+
|
|
176
|
+
def decorator(fn: HookFunc) -> HookFunc:
|
|
177
|
+
self.register_startup(fn, priority, abort_on_exception, timeout)
|
|
178
|
+
return fn
|
|
179
|
+
|
|
180
|
+
return decorator(func) if func else decorator
|
|
181
|
+
|
|
182
|
+
@overload
|
|
183
|
+
def on_shutdown(self, func: HookFunc, /) -> HookFunc: ...
|
|
184
|
+
|
|
185
|
+
@overload
|
|
186
|
+
def on_shutdown(
|
|
187
|
+
self,
|
|
188
|
+
*,
|
|
189
|
+
priority: int = DEFAULT_PRIORITY,
|
|
190
|
+
abort_on_exception: bool = SHUTDOWN_ABORT_ON_EXCEPTION,
|
|
191
|
+
timeout: int | float | None = None,
|
|
192
|
+
) -> Callable[[HookFunc], HookFunc]: ...
|
|
193
|
+
|
|
194
|
+
def on_shutdown(
|
|
195
|
+
self,
|
|
196
|
+
func: HookFunc | None = None,
|
|
197
|
+
*,
|
|
198
|
+
priority: int = DEFAULT_PRIORITY,
|
|
199
|
+
abort_on_exception: bool = SHUTDOWN_ABORT_ON_EXCEPTION,
|
|
200
|
+
timeout: int | float | None = None,
|
|
201
|
+
) -> Callable[[HookFunc], HookFunc] | HookFunc:
|
|
202
|
+
"""关闭钩子装饰器。
|
|
203
|
+
|
|
204
|
+
可带参数调用:@registry.on_shutdown(priority=100)
|
|
205
|
+
或不带参数:@registry.on_shutdown
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
func: 被装饰的函数
|
|
209
|
+
priority: 优先级(越大越先执行)
|
|
210
|
+
abort_on_exception: 异常时是否终止
|
|
211
|
+
timeout: 超时秒数
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
装饰器或原函数(取决于调用方式)
|
|
215
|
+
"""
|
|
216
|
+
|
|
217
|
+
def decorator(fn: HookFunc) -> HookFunc:
|
|
218
|
+
self.register_shutdown(fn, priority, abort_on_exception, timeout)
|
|
219
|
+
return fn
|
|
220
|
+
|
|
221
|
+
return decorator(func) if func else decorator
|
|
222
|
+
|
|
223
|
+
async def run_startup(self, app: FastAPI) -> None:
|
|
224
|
+
"""执行所有启动钩子。
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
app: FastAPI 应用实例
|
|
228
|
+
"""
|
|
229
|
+
if self._startup_hooks:
|
|
230
|
+
self._logger.info(f'正在执行 {len(self._startup_hooks)} 个启动钩子 (注册表 {id(self)})...')
|
|
231
|
+
await self._run_hooks(self._startup_hooks, app)
|
|
232
|
+
self._logger.info('启动钩子执行完成')
|
|
233
|
+
|
|
234
|
+
async def run_shutdown(self, app: FastAPI) -> None:
|
|
235
|
+
"""执行所有关闭钩子。
|
|
236
|
+
|
|
237
|
+
Args:
|
|
238
|
+
app: FastAPI 应用实例
|
|
239
|
+
"""
|
|
240
|
+
if self._shutdown_hooks:
|
|
241
|
+
self._logger.info(f'正在执行 {len(self._shutdown_hooks)} 个关闭钩子 (注册表 {id(self)})...')
|
|
242
|
+
await self._run_hooks(self._shutdown_hooks, app)
|
|
243
|
+
self._logger.info('关闭钩子执行完成')
|
|
244
|
+
|
|
245
|
+
def clear(self) -> None:
|
|
246
|
+
"""清空当前注册表中的所有钩子(包括启动和关闭)。
|
|
247
|
+
|
|
248
|
+
此方法会清空所有钩子列表和去重集合,用于测试环境隔离。
|
|
249
|
+
"""
|
|
250
|
+
self._startup_hooks.clear()
|
|
251
|
+
self._shutdown_hooks.clear()
|
|
252
|
+
self._startup_seen.clear()
|
|
253
|
+
self._shutdown_seen.clear()
|
|
254
|
+
|
|
255
|
+
def list_startup_hooks(self) -> list[str]:
|
|
256
|
+
"""返回按执行顺序排列的启动钩子描述列表。
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
钩子描述字符串列表,顺序即为执行顺序。
|
|
260
|
+
"""
|
|
261
|
+
return [self._format_hook(item) for item in self._startup_hooks]
|
|
262
|
+
|
|
263
|
+
def list_shutdown_hooks(self) -> list[str]:
|
|
264
|
+
"""返回按执行顺序排列的关闭钩子描述列表。
|
|
265
|
+
|
|
266
|
+
Returns:
|
|
267
|
+
钩子描述字符串列表,顺序即为执行顺序。
|
|
268
|
+
"""
|
|
269
|
+
return [self._format_hook(item) for item in self._shutdown_hooks]
|
|
270
|
+
|
|
271
|
+
def _register(
|
|
272
|
+
self,
|
|
273
|
+
hooks: list[_HookItem],
|
|
274
|
+
seen: set[int],
|
|
275
|
+
func: HookFunc,
|
|
276
|
+
priority: int,
|
|
277
|
+
abort_on_exception: bool,
|
|
278
|
+
timeout: int | float | None,
|
|
279
|
+
reverse_sort: bool,
|
|
280
|
+
) -> None:
|
|
281
|
+
"""通用注册方法,供启动/关闭钩子复用。
|
|
282
|
+
|
|
283
|
+
Args:
|
|
284
|
+
hooks: 目标钩子列表(启动或关闭)
|
|
285
|
+
seen: 已注册函数 id 集合(用于去重)
|
|
286
|
+
func: 待注册的钩子函数
|
|
287
|
+
priority: 执行优先级
|
|
288
|
+
abort_on_exception: 异常时是否终止执行
|
|
289
|
+
timeout: 超时秒数
|
|
290
|
+
reverse_sort: 排序方向(True 降序,False 升序)
|
|
291
|
+
"""
|
|
292
|
+
func_id = id(func)
|
|
293
|
+
if func_id in seen:
|
|
294
|
+
self._logger.debug(f'钩子 {func.__name__} 已注册,跳过重复注册')
|
|
295
|
+
return
|
|
296
|
+
seen.add(func_id)
|
|
297
|
+
|
|
298
|
+
# 校验必须是异步函数
|
|
299
|
+
if not iscoroutinefunction(func):
|
|
300
|
+
raise TypeError(f'生命周期钩子 `{func.__name__}` 必须是async异步函数')
|
|
301
|
+
|
|
302
|
+
try:
|
|
303
|
+
sig = signature(func)
|
|
304
|
+
needs_app = len(sig.parameters) > 0
|
|
305
|
+
except ValueError:
|
|
306
|
+
# 无法解析签名,保守认为需要app参数,避免静默错误
|
|
307
|
+
needs_app = True
|
|
308
|
+
|
|
309
|
+
item = _HookItem(func, priority, abort_on_exception, needs_app, timeout)
|
|
310
|
+
hooks.append(item)
|
|
311
|
+
hooks.sort(key=lambda x: x.priority, reverse=reverse_sort)
|
|
312
|
+
|
|
313
|
+
async def _run_hooks(self, hooks: list[_HookItem], app: FastAPI) -> None:
|
|
314
|
+
"""按序执行给定的钩子列表。
|
|
315
|
+
|
|
316
|
+
Args:
|
|
317
|
+
hooks: 钩子条目列表
|
|
318
|
+
app: FastAPI 应用实例
|
|
319
|
+
|
|
320
|
+
Raises:
|
|
321
|
+
RuntimeError: 当钩子超时或异常且 abort_on_exception=True 时抛出
|
|
322
|
+
"""
|
|
323
|
+
for item in hooks:
|
|
324
|
+
name = item.func.__name__
|
|
325
|
+
timeout: int | float | None = item.timeout
|
|
326
|
+
try:
|
|
327
|
+
coro = item.func(app) if item.needs_app else item.func()
|
|
328
|
+
if timeout is not None:
|
|
329
|
+
await asyncio.wait_for(coro, timeout=timeout)
|
|
330
|
+
else:
|
|
331
|
+
await coro
|
|
332
|
+
except asyncio.TimeoutError:
|
|
333
|
+
self._logger.error(
|
|
334
|
+
f'[生命周期钩子执行超时] {name}: 超过 {timeout}s',
|
|
335
|
+
exc_info=False
|
|
336
|
+
)
|
|
337
|
+
if item.abort_on_exception:
|
|
338
|
+
raise RuntimeError(f'[启动/关闭终止:钩子 {name} 执行超时]')
|
|
339
|
+
except Exception as e:
|
|
340
|
+
self._logger.error(f'[生命周期钩子执行失败] {name}', exc_info=True)
|
|
341
|
+
if item.abort_on_exception:
|
|
342
|
+
raise RuntimeError(f'[启动/关闭终止:钩子 {name} 异常]') from e
|
|
343
|
+
|
|
344
|
+
@staticmethod
|
|
345
|
+
def _format_hook(item: _HookItem) -> str:
|
|
346
|
+
"""将单个钩子条目格式化为可读的字符串。
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
item: 钩子条目
|
|
350
|
+
|
|
351
|
+
Returns:
|
|
352
|
+
格式化后的字符串,如 'func_name(priority=50, abort=True, timeout=None)'
|
|
353
|
+
"""
|
|
354
|
+
if item.timeout is not None:
|
|
355
|
+
timeout_str = f'{item.timeout}s'
|
|
356
|
+
else:
|
|
357
|
+
timeout_str = 'None'
|
|
358
|
+
return (
|
|
359
|
+
f'{item.func.__name__}(priority={item.priority}, '
|
|
360
|
+
f'abort={item.abort_on_exception}, timeout={timeout_str})'
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
# ---------- 核心注册表(始终优先执行) ----------
|
|
365
|
+
core_registry = HookRegistry()
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
# ---------- FastAPI 生命周期 ----------
|
|
369
|
+
@asynccontextmanager
|
|
370
|
+
async def fastapi_lifespan(app: FastAPI) -> AsyncGenerator[None, None]:
|
|
371
|
+
"""FastAPI 生命周期管理,强制 core_registry 在首位执行。
|
|
372
|
+
|
|
373
|
+
从 app.state.registries 读取用户自定义注册表列表(可选),
|
|
374
|
+
并确保 core_registry 始终在列表最前面,以保证核心钩子优先执行。
|
|
375
|
+
|
|
376
|
+
启动时,按列表顺序执行每个注册表的 startup 钩子;
|
|
377
|
+
关闭时,按逆序执行 shutdown 钩子,保证资源释放顺序符合依赖关系。
|
|
378
|
+
|
|
379
|
+
Example:
|
|
380
|
+
app = FastAPI(lifespan=fastapi_lifespan)
|
|
381
|
+
app.state.registries = [db_registry, cache_registry]
|
|
382
|
+
# core_registry 会自动插入到列表首位
|
|
383
|
+
|
|
384
|
+
Args:
|
|
385
|
+
app: FastAPI 应用实例
|
|
386
|
+
|
|
387
|
+
Yields:
|
|
388
|
+
None
|
|
389
|
+
|
|
390
|
+
Raises:
|
|
391
|
+
TypeError: 当 app.state.registries 的类型不是 HookRegistry 或列表时
|
|
392
|
+
"""
|
|
393
|
+
registries = getattr(app.state, 'registries', None)
|
|
394
|
+
|
|
395
|
+
# 规范化输入
|
|
396
|
+
if registries is None:
|
|
397
|
+
registries = []
|
|
398
|
+
elif isinstance(registries, HookRegistry):
|
|
399
|
+
registries = [registries]
|
|
400
|
+
elif isinstance(registries, Sequence):
|
|
401
|
+
registries = list(registries)
|
|
402
|
+
else:
|
|
403
|
+
raise TypeError(
|
|
404
|
+
f'app.state.registries 必须为 HookRegistry 或列表,实际为 {type(registries).__name__}'
|
|
405
|
+
)
|
|
406
|
+
|
|
407
|
+
# 过滤掉列表中非HookRegistry实例,防止异常
|
|
408
|
+
registries = [r for r in registries if isinstance(r, HookRegistry)]
|
|
409
|
+
|
|
410
|
+
# 去重(保留首次出现顺序)
|
|
411
|
+
seen = set[int]()
|
|
412
|
+
unique = []
|
|
413
|
+
for reg in registries:
|
|
414
|
+
rid = id(reg)
|
|
415
|
+
if rid not in seen:
|
|
416
|
+
seen.add(rid)
|
|
417
|
+
unique.append(reg)
|
|
418
|
+
registries = unique
|
|
419
|
+
|
|
420
|
+
# ---- 强制 core_registry 在首位 ----
|
|
421
|
+
core_id = id(core_registry)
|
|
422
|
+
if core_id not in seen:
|
|
423
|
+
registries.insert(0, core_registry)
|
|
424
|
+
else:
|
|
425
|
+
# 如果已在列表中,移除原有位置再插入首位(保持其他顺序不变)
|
|
426
|
+
registries = [reg for reg in registries if id(reg) != core_id]
|
|
427
|
+
registries.insert(0, core_registry)
|
|
428
|
+
|
|
429
|
+
try:
|
|
430
|
+
# 执行启动钩子(按列表顺序)
|
|
431
|
+
for reg in registries:
|
|
432
|
+
await reg.run_startup(app)
|
|
433
|
+
yield # 应用运行期间
|
|
434
|
+
finally:
|
|
435
|
+
# 无论启动阶段是否报错,都执行关闭钩子,防止资源泄漏
|
|
436
|
+
for reg in reversed(registries):
|
|
437
|
+
await reg.run_shutdown(app)
|
|
438
|
+
|
|
439
|
+
|
|
440
|
+
# ---------- 测试辅助 ----------
|
|
441
|
+
def clear_hooks(registry: HookRegistry | None = None) -> None:
|
|
442
|
+
"""清空注册表钩子,默认清空 core_registry(用于测试隔离)。
|
|
443
|
+
|
|
444
|
+
Args:
|
|
445
|
+
registry: 需要清空的注册表实例;不传默认 core_registry
|
|
446
|
+
"""
|
|
447
|
+
if registry is None:
|
|
448
|
+
core_registry.clear()
|
|
449
|
+
else:
|
|
450
|
+
registry.clear()
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""
|
|
2
|
+
@Author : zarkhan
|
|
3
|
+
@CreateDate : 2026/9/6
|
|
4
|
+
@Description: 日志模块——request_id注入、uvicorn接管、多进程安全轮转、一键配置
|
|
5
|
+
"""
|
|
6
|
+
from .config import NORMAL_FORMAT, set_log_format, set_log_level, setup_logger
|
|
7
|
+
from .filters import UvicornNameRewriteFilter
|
|
8
|
+
from .handlers import (
|
|
9
|
+
MonthlyRotatingFileHandler,
|
|
10
|
+
MultiProcessTimedRotatingFileHandler,
|
|
11
|
+
YearlyRotatingFileHandler,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
# config
|
|
16
|
+
'NORMAL_FORMAT',
|
|
17
|
+
'setup_logger',
|
|
18
|
+
'set_log_level',
|
|
19
|
+
'set_log_format',
|
|
20
|
+
# filters
|
|
21
|
+
'UvicornNameRewriteFilter',
|
|
22
|
+
# handlers
|
|
23
|
+
'MultiProcessTimedRotatingFileHandler',
|
|
24
|
+
'MonthlyRotatingFileHandler',
|
|
25
|
+
'YearlyRotatingFileHandler',
|
|
26
|
+
]
|