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.
Files changed (50) hide show
  1. fastapi_augment/__init__.py +24 -0
  2. fastapi_augment/common/__init__.py +61 -0
  3. fastapi_augment/common/constants.py +26 -0
  4. fastapi_augment/common/exception_handlers.py +178 -0
  5. fastapi_augment/common/exceptions.py +162 -0
  6. fastapi_augment/common/utils/__init__.py +5 -0
  7. fastapi_augment/common/utils/strings.py +175 -0
  8. fastapi_augment/config/__init__.py +8 -0
  9. fastapi_augment/config/settings.py +104 -0
  10. fastapi_augment/db/__init__.py +5 -0
  11. fastapi_augment/db/sqlalchemy/__init__.py +20 -0
  12. fastapi_augment/db/sqlalchemy/alembic/__init__.py +5 -0
  13. fastapi_augment/db/sqlalchemy/alembic/env.py +141 -0
  14. fastapi_augment/db/sqlalchemy/base.py +9 -0
  15. fastapi_augment/db/sqlalchemy/crud_base.py +426 -0
  16. fastapi_augment/db/sqlalchemy/engine.py +238 -0
  17. fastapi_augment/db/sqlalchemy/migrate.py +356 -0
  18. fastapi_augment/db/sqlalchemy/mixins/__init__.py +18 -0
  19. fastapi_augment/db/sqlalchemy/mixins/audit.py +61 -0
  20. fastapi_augment/db/sqlalchemy/mixins/soft_delete.py +80 -0
  21. fastapi_augment/db/sqlalchemy/mixins/timestamp.py +48 -0
  22. fastapi_augment/db/sqlalchemy/model_base.py +47 -0
  23. fastapi_augment/db/sqlalchemy/session.py +160 -0
  24. fastapi_augment/factory.py +238 -0
  25. fastapi_augment/health/__init__.py +34 -0
  26. fastapi_augment/health/checker.py +101 -0
  27. fastapi_augment/health/checkers.py +109 -0
  28. fastapi_augment/health/router.py +87 -0
  29. fastapi_augment/lifespan.py +450 -0
  30. fastapi_augment/log/__init__.py +26 -0
  31. fastapi_augment/log/config.py +201 -0
  32. fastapi_augment/log/factory.py +32 -0
  33. fastapi_augment/log/filters.py +23 -0
  34. fastapi_augment/log/handlers.py +81 -0
  35. fastapi_augment/middlewares/__init__.py +20 -0
  36. fastapi_augment/middlewares/base.py +79 -0
  37. fastapi_augment/middlewares/request_id.py +82 -0
  38. fastapi_augment/openapi.py +110 -0
  39. fastapi_augment/py.typed +0 -0
  40. fastapi_augment/schemas/__init__.py +29 -0
  41. fastapi_augment/schemas/base.py +32 -0
  42. fastapi_augment/schemas/pagination.py +46 -0
  43. fastapi_augment/schemas/request.py +28 -0
  44. fastapi_augment/schemas/response.py +139 -0
  45. fastapi_augment/schemas/types.py +11 -0
  46. fastapi_augment-0.1.0.dist-info/METADATA +654 -0
  47. fastapi_augment-0.1.0.dist-info/RECORD +50 -0
  48. fastapi_augment-0.1.0.dist-info/WHEEL +5 -0
  49. fastapi_augment-0.1.0.dist-info/entry_points.txt +2 -0
  50. fastapi_augment-0.1.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,24 @@
1
+ """
2
+ @Author : hangu
3
+ @CreateDate : 2026/8/31
4
+ @Description : fastapi-augment — FastAPI 通用代码工具包,跨项目复用
5
+ """
6
+ from .factory import create_app
7
+ from .lifespan import (
8
+ HookFunc,
9
+ HookRegistry,
10
+ core_registry,
11
+ fastapi_lifespan,
12
+ clear_hooks
13
+ )
14
+
15
+ __all__ = [
16
+ # factory
17
+ 'create_app',
18
+ # lifespan
19
+ 'HookFunc',
20
+ 'HookRegistry',
21
+ 'core_registry',
22
+ 'fastapi_lifespan',
23
+ 'clear_hooks'
24
+ ]
@@ -0,0 +1,61 @@
1
+ """
2
+ @Author : hangu
3
+ @CreateDate : 2026/9/4
4
+ @Description : 通用模块 — 异常 / 异常处理器 / 常量
5
+ """
6
+ from .constants import DEFAULT_ERR_MSG
7
+ from .exceptions import (
8
+ BaseHttpError,
9
+ BadRequestError,
10
+ UnauthorizedError,
11
+ PaymentRequiredError,
12
+ ForbiddenError,
13
+ NotFoundError,
14
+ MethodNotAllowedError,
15
+ NotAcceptableError,
16
+ RequestTimeoutError,
17
+ ConflictError,
18
+ GoneError,
19
+ PreconditionFailedError,
20
+ PayloadTooLargeError,
21
+ URITooLongError,
22
+ UnsupportedMediaTypeError,
23
+ LockedError,
24
+ TooManyRequestsError
25
+ )
26
+ from .exception_handlers import (
27
+ base_http_error_handler,
28
+ http_exception_handler,
29
+ validation_exception_handler,
30
+ general_exception_handler,
31
+ register_exception_handlers
32
+ )
33
+
34
+ __all__ = [
35
+ # constants
36
+ 'DEFAULT_ERR_MSG',
37
+ # exceptions
38
+ 'BaseHttpError',
39
+ 'BadRequestError',
40
+ 'UnauthorizedError',
41
+ 'PaymentRequiredError',
42
+ 'ForbiddenError',
43
+ 'NotFoundError',
44
+ 'MethodNotAllowedError',
45
+ 'NotAcceptableError',
46
+ 'RequestTimeoutError',
47
+ 'ConflictError',
48
+ 'GoneError',
49
+ 'PreconditionFailedError',
50
+ 'PayloadTooLargeError',
51
+ 'URITooLongError',
52
+ 'UnsupportedMediaTypeError',
53
+ 'LockedError',
54
+ 'TooManyRequestsError',
55
+ # exception handlers
56
+ 'base_http_error_handler',
57
+ 'http_exception_handler',
58
+ 'validation_exception_handler',
59
+ 'general_exception_handler',
60
+ 'register_exception_handlers'
61
+ ]
@@ -0,0 +1,26 @@
1
+ """
2
+ @Author : hangu
3
+ @CreateDate : 2026/9/4
4
+ @Description :
5
+ """
6
+ from starlette import status
7
+
8
+ # ===================== 统一默认文案常量(便于统一修改/国际化) =====================
9
+ DEFAULT_ERR_MSG: dict[int, str] = {
10
+ status.HTTP_400_BAD_REQUEST: 'Bad request',
11
+ status.HTTP_401_UNAUTHORIZED: 'Unauthorized',
12
+ status.HTTP_402_PAYMENT_REQUIRED: 'Payment required',
13
+ status.HTTP_403_FORBIDDEN: 'Forbidden',
14
+ status.HTTP_404_NOT_FOUND: 'Not found',
15
+ status.HTTP_405_METHOD_NOT_ALLOWED: 'Method not allowed',
16
+ status.HTTP_406_NOT_ACCEPTABLE: 'Not acceptable',
17
+ status.HTTP_408_REQUEST_TIMEOUT: 'Request timeout',
18
+ status.HTTP_409_CONFLICT: 'Conflict',
19
+ status.HTTP_410_GONE: 'Gone',
20
+ status.HTTP_412_PRECONDITION_FAILED: 'Precondition failed',
21
+ status.HTTP_413_CONTENT_TOO_LARGE: 'Payload too large',
22
+ status.HTTP_414_URI_TOO_LONG: 'URI too long',
23
+ status.HTTP_415_UNSUPPORTED_MEDIA_TYPE: 'Unsupported media type',
24
+ status.HTTP_423_LOCKED: 'Locked',
25
+ status.HTTP_429_TOO_MANY_REQUESTS: 'Too many requests',
26
+ }
@@ -0,0 +1,178 @@
1
+ """
2
+ @Author : hangu
3
+ @CreateDate : 2026/9/4
4
+ @Description : 全局统一异常处理器
5
+ - 将各类异常转换为统一的 APIResponse JSON 格式
6
+ - 提供 register_exception_handlers 一键注册到 FastAPI 应用
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from logging import getLogger
11
+
12
+ from fastapi import FastAPI
13
+ from fastapi.exceptions import RequestValidationError
14
+ from fastapi.responses import JSONResponse
15
+ from starlette.exceptions import HTTPException
16
+ from starlette.requests import Request
17
+
18
+ from .exceptions import BaseHttpError
19
+ from ..schemas.response import response_fail
20
+
21
+ _logger = getLogger(__name__)
22
+
23
+
24
+ # ===================== 业务异常处理器 =====================
25
+
26
+ async def base_http_error_handler(
27
+ _request: Request,
28
+ exc: BaseHttpError,
29
+ ) -> JSONResponse:
30
+ """处理 BaseHttpError 及其所有子类(统一业务异常)。
31
+
32
+ 将业务异常转换为统一 APIResponse 格式,
33
+ HTTP 状态码与 exc.status_code 一致,
34
+ body.code 同样使用 HTTP 状态码,body.message 使用 exc.detail。
35
+
36
+ Args:
37
+ _request: Starlette Request 对象
38
+ exc: 业务异常实例
39
+
40
+ Returns:
41
+ 统一格式的 JSON 响应
42
+ """
43
+ body = response_fail(code=exc.status_code, message=exc.detail or '请求错误')
44
+ return JSONResponse(
45
+ status_code=exc.status_code,
46
+ content=body.model_dump(mode='json', by_alias=True),
47
+ headers=exc.headers,
48
+ )
49
+
50
+
51
+ # ===================== 通用 HTTP 异常处理器 =====================
52
+
53
+ async def http_exception_handler(
54
+ _request: Request,
55
+ exc: HTTPException,
56
+ ) -> JSONResponse:
57
+ """处理 Starlette/FastAPI 原生 HTTPException。
58
+
59
+ 覆盖 FastAPI 默认处理器,将响应格式统一为 APIResponse。
60
+ 注意:BaseHttpError 继承自 HTTPException,但 FastAPI 会优先匹配
61
+ 更具体的处理器(base_http_error_handler),所以此处不会拦截业务异常。
62
+
63
+ Args:
64
+ _request: Starlette Request 对象
65
+ exc: HTTPException 实例
66
+
67
+ Returns:
68
+ 统一格式的 JSON 响应
69
+ """
70
+ body = response_fail(
71
+ code=exc.status_code,
72
+ message=exc.detail if isinstance(exc.detail, str) else str(exc.detail or ''),
73
+ )
74
+ return JSONResponse(
75
+ status_code=exc.status_code,
76
+ content=body.model_dump(mode='json', by_alias=True),
77
+ headers=getattr(exc, 'headers', None),
78
+ )
79
+
80
+
81
+ # ===================== 请求校验异常处理器 =====================
82
+
83
+ async def validation_exception_handler(
84
+ _request: Request,
85
+ exc: RequestValidationError,
86
+ ) -> JSONResponse:
87
+ """处理 Pydantic 请求参数校验异常(422)。
88
+
89
+ 将校验错误详情提取到 extra.errors 中,方便前端定位具体字段。
90
+
91
+ Args:
92
+ _request: Starlette Request 对象
93
+ exc: RequestValidationError 实例
94
+
95
+ Returns:
96
+ 统一格式的 JSON 响应(HTTP 422)
97
+ """
98
+ errors = []
99
+ for error in exc.errors():
100
+ field = ' -> '.join(str(loc) for loc in error['loc'])
101
+ errors.append({
102
+ 'field': field,
103
+ 'message': error['msg'],
104
+ 'type': error['type'],
105
+ })
106
+
107
+ body = response_fail(
108
+ code=422,
109
+ message='请求参数校验失败',
110
+ extra={'errors': errors},
111
+ )
112
+ return JSONResponse(
113
+ status_code=422,
114
+ content=body.model_dump(mode='json', by_alias=True),
115
+ )
116
+
117
+
118
+ # ===================== 未知异常处理器 =====================
119
+
120
+ async def general_exception_handler(
121
+ _request: Request,
122
+ exc: Exception,
123
+ ) -> JSONResponse:
124
+ """处理所有未被捕获的异常(兜底)。
125
+
126
+ 记录完整异常日志(含堆栈),但响应体只返回通用提示,
127
+ 避免将内部实现细节(堆栈、SQL 等)暴露给客户端。
128
+
129
+ Args:
130
+ _request: Starlette Request 对象
131
+ exc: 未捕获的异常实例
132
+
133
+ Returns:
134
+ 统一格式的 JSON 响应(HTTP 500)
135
+ """
136
+ _logger.exception(f'[未捕获异常] {type(exc).__name__}: {exc}')
137
+ body = response_fail(code=500, message='服务器内部错误')
138
+ return JSONResponse(
139
+ status_code=500,
140
+ content=body.model_dump(mode='json', by_alias=True),
141
+ )
142
+
143
+
144
+ # ===================== 一键注册 =====================
145
+
146
+ def register_exception_handlers(app: FastAPI) -> None:
147
+ """将全部统一异常处理器注册到 FastAPI 应用。
148
+
149
+ 注册后,以下异常会被转换为统一的 APIResponse 格式:
150
+ - BaseHttpError 及子类 → 对应 HTTP 状态码
151
+ - Starlette HTTPException → 对应 HTTP 状态码
152
+ - RequestValidationError → HTTP 422 + 字段级错误详情
153
+ - Exception(兜底) → HTTP 500
154
+
155
+ 注意:create_app 工厂默认自动调用本函数(register_exceptions=True),
156
+ 通常无需手动注册。仅在以下场景需要手动调用:
157
+ - 未使用 create_app 工厂,自行构建 FastAPI 实例时
158
+ - 传入 register_exceptions=False 禁用后,想选择性注册时
159
+
160
+ Example::
161
+
162
+ # 方式一:工厂自动注册(推荐)
163
+ app = create_app(title='My Service')
164
+
165
+ # 方式二:手动注册(未使用工厂时)
166
+ from fastapi import FastAPI
167
+ app = FastAPI()
168
+ register_exception_handlers(app)
169
+
170
+ Args:
171
+ app: FastAPI 应用实例
172
+ """
173
+ # 注意注册顺序:BaseHttpError 必须在 HTTPException 之前注册,
174
+ # 因为 BaseHttpError 继承自 HTTPException,FastAPI 优先匹配更具体的异常类型
175
+ app.add_exception_handler(BaseHttpError, base_http_error_handler) # type: ignore
176
+ app.add_exception_handler(HTTPException, http_exception_handler) # type: ignore
177
+ app.add_exception_handler(RequestValidationError, validation_exception_handler) # type: ignore
178
+ app.add_exception_handler(Exception, general_exception_handler) # type: ignore
@@ -0,0 +1,162 @@
1
+ """
2
+ @Author : hangu
3
+ @CreateDate : 2026/9/4
4
+ @Description : 统一业务异常
5
+ """
6
+ from typing import Any
7
+
8
+ from starlette import status
9
+ from starlette.exceptions import HTTPException
10
+
11
+ from .constants import DEFAULT_ERR_MSG
12
+
13
+
14
+ # ===================== 通用基类:统一封装 detail + headers 逻辑 =====================
15
+ class BaseHttpError(HTTPException):
16
+ """统一HTTP异常基类,所有4xx异常继承此类,原生兼容starlette.HTTPException.
17
+
18
+ 子类只需声明 ``_status_code`` 类变量,无需重写 __init__::
19
+
20
+ class NotFoundError(BaseHttpError):
21
+ _status_code = 404
22
+
23
+ raise NotFoundError() # 使用默认文案
24
+ raise NotFoundError(detail='用户不存在') # 自定义提示
25
+
26
+ Attributes:
27
+ _status_code: 子类声明的HTTP状态码
28
+ status_code: HTTP状态码(继承自HTTPException)
29
+ detail: 异常提示文案
30
+ headers: 响应附加http头
31
+ """
32
+ __slots__ = ()
33
+ _status_code: int | None = None
34
+
35
+ def __init__(
36
+ self,
37
+ status_code: int | None = None,
38
+ detail: str | None = None,
39
+ headers: dict[str, Any] | None = None,
40
+ ):
41
+ """初始化http业务异常.
42
+
43
+ Args:
44
+ status_code: http响应状态码,不传则读取子类的 _status_code 类变量
45
+ detail: 自定义错误提示,不传使用内置默认文案
46
+ headers: 附加响应头
47
+
48
+ Raises:
49
+ KeyError: 传入未在DEFAULT_ERR_MSG定义的status_code
50
+ """
51
+ code = status_code if status_code is not None else self._status_code
52
+ if code is None:
53
+ raise TypeError(
54
+ f'{type(self).__name__} 必须声明 _status_code 类变量或传入 status_code 参数'
55
+ )
56
+ if code not in DEFAULT_ERR_MSG:
57
+ raise KeyError(f'status_code {code} not defined in DEFAULT_ERR_MSG')
58
+ msg = detail or DEFAULT_ERR_MSG[code]
59
+ super().__init__(status_code=code, detail=msg, headers=headers)
60
+
61
+
62
+ # ===================== 各类4xx异常子类(极简声明,无重复__init__) =====================
63
+ # 子类只需声明 _status_code 类变量,__init__ 由基类统一处理
64
+ class BadRequestError(BaseHttpError):
65
+ """400 请求错误."""
66
+ _status_code = status.HTTP_400_BAD_REQUEST
67
+
68
+
69
+ class UnauthorizedError(BaseHttpError):
70
+ """401 未授权错误."""
71
+ _status_code = status.HTTP_401_UNAUTHORIZED
72
+
73
+
74
+ class PaymentRequiredError(BaseHttpError):
75
+ """402 需要付费错误."""
76
+ _status_code = status.HTTP_402_PAYMENT_REQUIRED
77
+
78
+
79
+ class ForbiddenError(BaseHttpError):
80
+ """403 禁止访问错误."""
81
+ _status_code = status.HTTP_403_FORBIDDEN
82
+
83
+
84
+ class NotFoundError(BaseHttpError):
85
+ """404 未找到资源."""
86
+ _status_code = status.HTTP_404_NOT_FOUND
87
+
88
+
89
+ class MethodNotAllowedError(BaseHttpError):
90
+ """405 请求方法不允许."""
91
+ _status_code = status.HTTP_405_METHOD_NOT_ALLOWED
92
+
93
+
94
+ class NotAcceptableError(BaseHttpError):
95
+ """406 客户端不支持返回格式."""
96
+ _status_code = status.HTTP_406_NOT_ACCEPTABLE
97
+
98
+
99
+ class RequestTimeoutError(BaseHttpError):
100
+ """408 请求超时."""
101
+ _status_code = status.HTTP_408_REQUEST_TIMEOUT
102
+
103
+
104
+ class ConflictError(BaseHttpError):
105
+ """409 资源冲突."""
106
+ _status_code = status.HTTP_409_CONFLICT
107
+
108
+
109
+ class GoneError(BaseHttpError):
110
+ """410 资源已永久删除."""
111
+ _status_code = status.HTTP_410_GONE
112
+
113
+
114
+ class PreconditionFailedError(BaseHttpError):
115
+ """412 前置校验失败."""
116
+ _status_code = status.HTTP_412_PRECONDITION_FAILED
117
+
118
+
119
+ class PayloadTooLargeError(BaseHttpError):
120
+ """413 请求体过大."""
121
+ _status_code = status.HTTP_413_CONTENT_TOO_LARGE
122
+
123
+
124
+ class URITooLongError(BaseHttpError):
125
+ """414 URI链接过长."""
126
+ _status_code = status.HTTP_414_URI_TOO_LONG
127
+
128
+
129
+ class UnsupportedMediaTypeError(BaseHttpError):
130
+ """415 不支持的请求媒体类型."""
131
+ _status_code = status.HTTP_415_UNSUPPORTED_MEDIA_TYPE
132
+
133
+
134
+ class LockedError(BaseHttpError):
135
+ """423 资源锁定."""
136
+ _status_code = status.HTTP_423_LOCKED
137
+
138
+
139
+ class TooManyRequestsError(BaseHttpError):
140
+ """429 请求过于频繁(限流专用,支持retry_after快捷参数)."""
141
+ _status_code = status.HTTP_429_TOO_MANY_REQUESTS
142
+
143
+ def __init__(
144
+ self,
145
+ detail: str | None = None,
146
+ retry_after: int | None = None,
147
+ headers: dict[str, Any] | None = None,
148
+ ):
149
+ """
150
+
151
+ Args:
152
+ detail: 自定义错误提示
153
+ retry_after: 设置 Retry‑After 响应头,单位秒
154
+ headers: 自定义附加响应头
155
+ """
156
+ final_headers: dict[str, Any] = dict(headers) if headers is not None else {}
157
+ if retry_after is not None:
158
+ final_headers['Retry-After'] = str(retry_after)
159
+ super().__init__(
160
+ detail=detail,
161
+ headers=final_headers if final_headers else None,
162
+ )
@@ -0,0 +1,5 @@
1
+ """
2
+ @Author : hangu
3
+ @CreateDate : 2026/9/4
4
+ @Description :
5
+ """
@@ -0,0 +1,175 @@
1
+ """
2
+ @Author : hangu
3
+ @CreateDate : 2026/9/4
4
+ @Description :
5
+ """
6
+ import json
7
+ import random
8
+ import string
9
+ from typing import Any, Protocol
10
+
11
+
12
+ # ── 类型定义 ──
13
+ class SupportsWriteStr(Protocol):
14
+ def write(self, s: str) -> object: ...
15
+
16
+
17
+ class SupportsReadBytes(Protocol):
18
+ def read(self) -> bytes: ...
19
+
20
+
21
+ # ── orjson 可选加载 ──
22
+ ORJSON_INSTALLED: bool = False
23
+ ORJSON_DEFAULT_OPTS: int = 0
24
+ _oj_dumps: Any = None
25
+ _oj_loads: Any = None
26
+
27
+ try:
28
+ import orjson as _orjson # type: ignore[import-untyped]
29
+ ORJSON_INSTALLED = True
30
+ ORJSON_DEFAULT_OPTS = _orjson.OPT_SERIALIZE_NUMPY | _orjson.OPT_UTC_Z
31
+ _oj_dumps = _orjson.dumps
32
+ _oj_loads = _orjson.loads
33
+ except ImportError:
34
+ pass
35
+
36
+
37
+ # orjson OPT_INDENT_2 的常量值,避免直接依赖 orjson 安装
38
+ _ORJSON_OPT_INDENT_2 = 0x04
39
+
40
+
41
+ # ── 字符串转换 ──
42
+ def camel_to_snake(s: str) -> str:
43
+ """驼峰转下划线(支持连续大写)"""
44
+ if not s:
45
+ return s
46
+ result: list[str] = []
47
+ prev_lower = False
48
+ for i, c in enumerate(s):
49
+ if c.isupper():
50
+ if i > 0 and (prev_lower or (i + 1 < len(s) and s[i + 1].islower())):
51
+ result.append('_')
52
+ result.append(c.lower())
53
+ prev_lower = False
54
+ else:
55
+ result.append(c)
56
+ prev_lower = c.islower()
57
+ return ''.join(result)
58
+
59
+
60
+ def snake_to_camel(s: str) -> str:
61
+ """下划线转驼峰(首字母大写)"""
62
+ if not s:
63
+ return s
64
+ return ''.join(part.capitalize() for part in s.split('_'))
65
+
66
+
67
+ # ── 随机字符串 ──
68
+ def random_string(
69
+ length: int = 16,
70
+ chars: str | None = None,
71
+ exclude: str | None = None,
72
+ ) -> str:
73
+ """生成随机字符串,支持自定义字符集
74
+
75
+ Raises:
76
+ ValueError: 字符集为空时(exclude 排除了所有字符)
77
+ """
78
+ if chars is None:
79
+ chars = string.ascii_letters + string.digits
80
+ if exclude:
81
+ chars = ''.join(c for c in chars if c not in exclude)
82
+ if not chars:
83
+ raise ValueError('字符集为空,无法生成随机字符串(exclude 排除了所有字符)')
84
+ return ''.join(random.choices(chars, k=length))
85
+
86
+
87
+ # ── JSON 序列化 ──
88
+ def json_dumps(obj: Any, compact: bool = True) -> str:
89
+ """高性能 JSON 序列化
90
+
91
+ Args:
92
+ obj: 要序列化的对象
93
+ compact: 是否压缩输出(去除空格)
94
+
95
+ Returns:
96
+ JSON 字符串
97
+
98
+ Raises:
99
+ TypeError: 当对象不可序列化时
100
+ """
101
+ if ORJSON_INSTALLED:
102
+ try:
103
+ opts = ORJSON_DEFAULT_OPTS
104
+ if not compact:
105
+ opts |= _ORJSON_OPT_INDENT_2
106
+ return _oj_dumps(obj, option=opts).decode('utf-8')
107
+ except (TypeError, ValueError) as e:
108
+ raise TypeError(f'JSON 序列化失败: {e}') from e
109
+ if compact:
110
+ return json.dumps(obj, ensure_ascii=False, separators=(',', ':'))
111
+ return json.dumps(obj, ensure_ascii=False, indent=2)
112
+
113
+
114
+ def json_dump(obj: Any, fp: SupportsWriteStr, compact: bool = True) -> None:
115
+ """高性能 JSON 写入文件
116
+
117
+ Args:
118
+ obj: 要序列化的对象
119
+ fp: 可写文件对象
120
+ compact: 是否压缩输出
121
+ """
122
+ fp.write(json_dumps(obj, compact=compact))
123
+
124
+
125
+ def json_loads(s: str | bytes) -> Any:
126
+ """高性能 JSON 反序列化
127
+
128
+ Args:
129
+ s: JSON 字符串或字节
130
+
131
+ Returns:
132
+ 反序列化后的 Python 对象
133
+
134
+ Raises:
135
+ json.JSONDecodeError: JSON 解析失败时
136
+ """
137
+ if ORJSON_INSTALLED:
138
+ try:
139
+ return _oj_loads(s)
140
+ except (TypeError, ValueError) as e:
141
+ text = s.decode('utf-8', errors='replace') if isinstance(s, bytes) else s
142
+ raise json.JSONDecodeError(str(e), text, 0) from e
143
+ if isinstance(s, bytes):
144
+ s = s.decode('utf-8')
145
+ try:
146
+ return json.loads(s)
147
+ except json.JSONDecodeError:
148
+ raise
149
+ except (TypeError, ValueError) as e:
150
+ raise json.JSONDecodeError(str(e), s, 0) from e
151
+
152
+
153
+ def json_load(fp: SupportsReadBytes) -> Any:
154
+ """从文件读取 JSON
155
+
156
+ Args:
157
+ fp: 可读字节文件对象
158
+
159
+ Returns:
160
+ 反序列化后的 Python 对象
161
+
162
+ Raises:
163
+ json.JSONDecodeError: JSON 解析失败时
164
+ OSError: 文件读取失败时
165
+ """
166
+ try:
167
+ raw = fp.read()
168
+ except (OSError, IOError) as e:
169
+ raise OSError(f'文件读取失败: {e}') from e
170
+ if ORJSON_INSTALLED:
171
+ try:
172
+ return _oj_loads(raw)
173
+ except (TypeError, ValueError) as e:
174
+ raise json.JSONDecodeError(str(e), raw.decode('utf-8', errors='replace'), 0) from e
175
+ return json.loads(raw.decode('utf-8') if isinstance(raw, bytes) else raw)
@@ -0,0 +1,8 @@
1
+ """
2
+ @Author : zarkhan
3
+ @CreateDate : 2026/9/6
4
+ @Description: 配置管理模块
5
+ """
6
+ from .settings import EnvSettings
7
+
8
+ __all__ = ['EnvSettings']