algo-backend-framework 0.0.1__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 (48) hide show
  1. algo_backend/__init__.py +0 -0
  2. algo_backend/config/__init__.py +8 -0
  3. algo_backend/config/basic_config.py +13 -0
  4. algo_backend/config/loguru_config.py +19 -0
  5. algo_backend/exception/__init__.py +22 -0
  6. algo_backend/exception/error_code_manage.py +126 -0
  7. algo_backend/exception/exception.py +42 -0
  8. algo_backend/exception/status_code.py +103 -0
  9. algo_backend/handler/__init__.py +3 -0
  10. algo_backend/handler/exception_to_vo.py +37 -0
  11. algo_backend/handler/operation_handler.py +71 -0
  12. algo_backend/intercept/__init__.py +9 -0
  13. algo_backend/intercept/common.py +45 -0
  14. algo_backend/intercept/http.py +40 -0
  15. algo_backend/intercept/validate.py +78 -0
  16. algo_backend/log/__init__.py +1 -0
  17. algo_backend/log/common.py +16 -0
  18. algo_backend/log/loguru/__init__.py +5 -0
  19. algo_backend/log/loguru/log_clean.py +140 -0
  20. algo_backend/log/loguru/log_setup.py +89 -0
  21. algo_backend/log/loguru/log_starter.py +65 -0
  22. algo_backend/log/loguru/patch_logging.py +83 -0
  23. algo_backend/log/nblog/__init__.py +0 -0
  24. algo_backend/metrics/__init__.py +22 -0
  25. algo_backend/metrics/collector/__init__.py +12 -0
  26. algo_backend/metrics/collector/common.py +17 -0
  27. algo_backend/metrics/collector/gc_metrics.py +74 -0
  28. algo_backend/metrics/collector/schedule_monitor.py +50 -0
  29. algo_backend/metrics/collector/system_metrics.py +169 -0
  30. algo_backend/metrics/http_metrics.py +56 -0
  31. algo_backend/metrics/prometheus_context.py +55 -0
  32. algo_backend/metrics/time_cost_metrics.py +146 -0
  33. algo_backend/middleware/__init__.py +4 -0
  34. algo_backend/middleware/cors.py +10 -0
  35. algo_backend/middleware/metrics.py +12 -0
  36. algo_backend/schema/__init__.py +3 -0
  37. algo_backend/schema/vo.py +83 -0
  38. algo_backend/starter/__init__.py +4 -0
  39. algo_backend/starter/default_app_generator.py +169 -0
  40. algo_backend/starter/default_service_starter.py +70 -0
  41. algo_backend/starter/event_list.py +32 -0
  42. algo_backend/utils/__init__.py +8 -0
  43. algo_backend/utils/meta_class.py +50 -0
  44. algo_backend/utils/utils.py +22 -0
  45. algo_backend_framework-0.0.1.dist-info/METADATA +60 -0
  46. algo_backend_framework-0.0.1.dist-info/RECORD +48 -0
  47. algo_backend_framework-0.0.1.dist-info/WHEEL +5 -0
  48. algo_backend_framework-0.0.1.dist-info/top_level.txt +1 -0
File without changes
@@ -0,0 +1,8 @@
1
+ from dotenv import load_dotenv
2
+
3
+ load_dotenv(".env")
4
+
5
+ from .basic_config import ErrorCodeConfig, ServiceConfig
6
+ from .loguru_config import LoguruConfig
7
+
8
+ __all__ = ["LoguruConfig", "ServiceConfig", "ErrorCodeConfig"]
@@ -0,0 +1,13 @@
1
+ import os
2
+
3
+ from algo_backend.utils import OsAttrMeta
4
+
5
+
6
+ class ErrorCodeConfig:
7
+ SERVICE_PREFIX: int = os.getenv("ERROR_CODE_SERVICE_PREFIX", "0")
8
+
9
+
10
+ class ServiceConfig(metaclass=OsAttrMeta):
11
+ HTTP_PORT: int = 8100
12
+ TIMEOUT_KEEP_ALIVE: int = 1000
13
+ PROCESS_NUM: int = 1
@@ -0,0 +1,19 @@
1
+ from typing import List
2
+
3
+ from algo_backend.utils import OsAttrMeta
4
+
5
+
6
+ class LoguruConfig(metaclass=OsAttrMeta):
7
+ LOGGER_PATH: str = "/logger"
8
+ LOG_RETENTION_DAY: int = 60
9
+ DISABLE_LOG_PKG: str = ""
10
+ LOG_ADD_CONTAINED_ID: bool = False
11
+ SAVE_INFO_LEVEL: bool = False
12
+ SAVE_DEBUG_LOG: bool = True
13
+
14
+ @classmethod
15
+ def get_disable_log_pkg(cls) -> List[str]:
16
+ if cls.DISABLE_LOG_PKG:
17
+ return cls.DISABLE_LOG_PKG.split(",")
18
+ else:
19
+ return []
@@ -0,0 +1,22 @@
1
+ from .error_code_manage import ApiErrorCodeManage, BasicCodeManage
2
+ from .exception import BasicApiInnerException, BasicCommonException, BasicException
3
+ from .status_code import (
4
+ BasicApiId,
5
+ BasicApiInnerErrorCode,
6
+ BasicStatusCode,
7
+ CommonStatusCode,
8
+ DefaultApiErrorCode,
9
+ )
10
+
11
+ __all__ = [
12
+ "BasicStatusCode",
13
+ "CommonStatusCode",
14
+ "BasicApiId",
15
+ "BasicApiInnerErrorCode",
16
+ "BasicApiInnerException",
17
+ "ApiErrorCodeManage",
18
+ "BasicCodeManage",
19
+ "BasicCommonException",
20
+ "DefaultApiErrorCode",
21
+ "BasicException",
22
+ ]
@@ -0,0 +1,126 @@
1
+ import inspect
2
+ import sys
3
+ from typing import Optional
4
+
5
+ from algo_backend.config import ErrorCodeConfig
6
+
7
+ from . import status_code
8
+ from .status_code import BasicApiId, BasicApiInnerErrorCode, BasicStatusCode
9
+
10
+
11
+ class ApiErrorCodeManage:
12
+ """
13
+ 构建”5aaabb“错误码,aaa接口id,bb表示接口内部错误码
14
+ """
15
+
16
+ @classmethod
17
+ def set_error_code_prefix_env(cls, prefix: Optional[int] = None):
18
+ """
19
+ 服务启动时执行这个函数
20
+ 也可以启动服务时通过环境变量ERROR_CODE_SERVICE_PREFIX指定服务前缀
21
+ 如果输入的prefix code不符合规则,则忽略,保持原来的6位
22
+ """
23
+ if prefix and 1 <= prefix <= 99:
24
+ ErrorCodeConfig.SERVICE_PREFIX = prefix
25
+
26
+ @classmethod
27
+ def scan_module_and_summary(cls, *list_module) -> list:
28
+ """扫描模块中的ApiId和错误码枚举类"""
29
+ api_id_dict: dict = {}
30
+ api_err_code_info = []
31
+
32
+ cls_list = []
33
+ for m in list_module:
34
+ cls_list.extend(
35
+ [obj for name, obj in inspect.getmembers(m) if inspect.isclass(obj)]
36
+ )
37
+
38
+ for obj in set(cls_list):
39
+ # 查找继承自BasicApiId的枚举类
40
+ if issubclass(obj, BasicApiId) and obj != BasicApiId:
41
+ for enum_member in obj:
42
+ api_id_dict[enum_member.value] = enum_member.name
43
+ api_err_code_info.append(
44
+ [
45
+ enum_member.value,
46
+ BasicApiInnerErrorCode.gen_standard_code(enum_member, 0),
47
+ "",
48
+ ]
49
+ )
50
+
51
+ # 查找继承自BasicApiInnerErrorCode的枚举类
52
+ elif (
53
+ issubclass(obj, BasicApiInnerErrorCode)
54
+ and obj != BasicApiInnerErrorCode
55
+ and hasattr(obj, "__api_id__")
56
+ ):
57
+ api_id_enum: BasicApiId = getattr(obj, "__api_id__")
58
+ for enum_member in obj:
59
+ api_err_code_info.append(
60
+ [
61
+ api_id_enum.value,
62
+ BasicApiInnerErrorCode.gen_standard_code(
63
+ api_id_enum, enum_member.value
64
+ ),
65
+ enum_member.msg,
66
+ ]
67
+ )
68
+
69
+ for idx, (api_code, _, _) in enumerate(api_err_code_info):
70
+ api_err_code_info[idx][0] = api_id_dict[api_code]
71
+
72
+ # 排序
73
+ api_err_code_info.sort(key=lambda x: x[1])
74
+
75
+ return api_err_code_info
76
+
77
+ @classmethod
78
+ def summary_module_api_error_code_markdown(cls, *list_module):
79
+ """
80
+ 打印模块中的ApiId和错误码枚举类
81
+ """
82
+ result = cls.scan_module_and_summary(*list_module)
83
+
84
+ print("| ApiName | ErrorCode | Message |")
85
+ print("|--------|------------|---------|")
86
+ for api_id, api_code, msg in result:
87
+ print(f"| {api_id} | {api_code} | {msg} |")
88
+
89
+
90
+ class BasicCodeManage:
91
+ __DEFAULT_MODULE = sys.modules[status_code.__name__]
92
+
93
+ @classmethod
94
+ def scan_module_and_summary(cls, *list_module) -> list:
95
+ """扫描模块中BasicStatusCode枚举类及其子类"""
96
+ if cls.__DEFAULT_MODULE not in list_module:
97
+ list_module = list_module + (cls.__DEFAULT_MODULE,)
98
+
99
+ result = []
100
+ cls_list = []
101
+
102
+ for m in list_module:
103
+ cls_list.extend(
104
+ [obj for name, obj in inspect.getmembers(m) if inspect.isclass(obj)]
105
+ )
106
+
107
+ for obj in set(cls_list):
108
+ if issubclass(obj, BasicStatusCode) and obj != BasicApiId:
109
+ for enum_member in obj:
110
+ result.append([enum_member.value, enum_member.msg])
111
+
112
+ result.sort(key=lambda x: x[0])
113
+
114
+ return result
115
+
116
+ @classmethod
117
+ def summary_error_code_markdown(cls, *list_module):
118
+ """
119
+ 打印模块中的ApiId和错误码枚举类
120
+ """
121
+ result = cls.scan_module_and_summary(*list_module)
122
+
123
+ print("| Code | Message |")
124
+ print("|--------|------------|")
125
+ for code, msg in result:
126
+ print(f"| {code} | {msg} |")
@@ -0,0 +1,42 @@
1
+ from typing import TypeVar
2
+
3
+ from .status_code import BasicApiId, BasicApiInnerErrorCode, BasicStatusCode
4
+
5
+
6
+ class BasicException(Exception):
7
+ """
8
+ 基础异常类
9
+ """
10
+
11
+ def __init__(self, code: int, msg: str):
12
+ super().__init__(msg)
13
+ self.code = code
14
+ self.msg = msg
15
+
16
+
17
+ # 定义泛型变量 T,限定为 BasicStatusCode 的子类
18
+ T = TypeVar("T", bound=BasicStatusCode)
19
+
20
+
21
+ class BasicCommonException(BasicException):
22
+ def __init__(self, status_code: T, **kwargs):
23
+ super().__init__(code=status_code.code, msg=status_code.msg.format(**kwargs))
24
+
25
+
26
+ # 定义泛型变量 ET,限定为 BasicApiInnerErrorCode 的子类
27
+ ET = TypeVar("ET", bound=BasicApiInnerErrorCode)
28
+
29
+
30
+ class BasicApiInnerException(BasicException):
31
+ """
32
+ 接口内部异常类的基类
33
+ """
34
+
35
+ def __init__(self, status_code: ET, **kwargs):
36
+ super().__init__(code=status_code.code, msg=status_code.msg.format(**kwargs))
37
+
38
+ def add_api_id(self, api_id: BasicApiId) -> "BasicApiInnerException":
39
+ self.code = BasicApiInnerErrorCode.gen_standard_code(
40
+ api_id=api_id, inner_error_code=self.code
41
+ )
42
+ return self
@@ -0,0 +1,103 @@
1
+ from enum import Enum
2
+
3
+ from algo_backend.config import ErrorCodeConfig
4
+
5
+
6
+ def add_service_prefix(v: int):
7
+ """
8
+ 对于失败的状态码,添加服务号码前缀
9
+ """
10
+ v_str = str(v)
11
+ if len(v_str) == 8:
12
+ return v
13
+ if v is not None and v != 0 and ErrorCodeConfig.SERVICE_PREFIX:
14
+ v = int(f"{ErrorCodeConfig.SERVICE_PREFIX}{v_str}")
15
+ return v
16
+
17
+
18
+ class BasicStatusCode(Enum):
19
+ def __new__(cls, value: int, msg: str):
20
+ assert 0 <= value <= 999999, f"错误码长度不能超过6位,且非负数,{value}"
21
+ obj = object.__new__(cls)
22
+ obj._value_ = obj
23
+ obj._code = value
24
+ obj.msg = msg
25
+ return obj
26
+
27
+ @property
28
+ def code(self):
29
+ return add_service_prefix(self._code)
30
+
31
+ @property
32
+ def value(self):
33
+ return self.code
34
+
35
+
36
+ class CommonStatusCode(BasicStatusCode):
37
+ SUCCESS = (0, "成功")
38
+
39
+ ERROR_REQ_URL = (400001, "请求路径错误")
40
+ ERROR_REQ_METHOD = (400002, "{url}请求方法错误")
41
+ BODY_EMPTY_ERR = (400003, "{url}请求体内容为空")
42
+ PARAM_ERROR = (400006, "接口{url}参数错误,{msg}")
43
+ ERROR_HEADER_NOW_EXISTS = (400007, "接口{url}缺少请求头:{header}")
44
+
45
+ UNKNOWN_ERROR = (500000, "未知错误: {msg}")
46
+
47
+
48
+ class BasicApiId(Enum):
49
+ """
50
+ 接口编号枚举类
51
+ """
52
+
53
+ def __new__(cls, value: int):
54
+ assert 0 <= value <= 999, f"接口编号长度不能超过3位,且非负数,{value}"
55
+ obj = object.__new__(cls)
56
+ obj._value_ = obj
57
+ obj.code = value
58
+ return obj
59
+
60
+ @property
61
+ def value(self):
62
+ return self.code
63
+
64
+ def bind_api(self):
65
+ """
66
+ 装饰器,绑定绑定api id
67
+ """
68
+
69
+ def decorator(enum_class):
70
+ enum_class.__api_id__ = self
71
+ return enum_class
72
+
73
+ return decorator
74
+
75
+
76
+ class DefaultApiErrorCode(BasicApiId):
77
+ """
78
+ 0表示不提供任何ApiId
79
+ """
80
+
81
+ DEFAULT_ERROR = 0
82
+
83
+
84
+ class BasicApiInnerErrorCode(Enum):
85
+ def __new__(cls, value: int, msg: str):
86
+ assert 1 <= value <= 99, f"接口内部错误码长度不能超过2位,且为正数,{value}"
87
+ obj = object.__new__(cls)
88
+ obj._value_ = obj
89
+ obj.code = value
90
+ obj.msg = msg
91
+ return obj
92
+
93
+ @property
94
+ def value(self):
95
+ return self.code
96
+
97
+ @classmethod
98
+ def gen_standard_code(cls, api_id: BasicApiId, inner_error_code: int = 0):
99
+ """
100
+ 拼接并生成6位错误码
101
+ """
102
+ api_id = api_id or DefaultApiErrorCode.DEFAULT_ERROR
103
+ return 500000 + api_id.code * 100 + inner_error_code
@@ -0,0 +1,3 @@
1
+ from .operation_handler import timing_and_exception_handler
2
+
3
+ __all__ = ["timing_and_exception_handler"]
@@ -0,0 +1,37 @@
1
+ import logging
2
+ from typing import Optional
3
+
4
+ from algo_backend.exception import (
5
+ BasicApiId,
6
+ BasicApiInnerException,
7
+ BasicCommonException,
8
+ CommonStatusCode,
9
+ )
10
+ from algo_backend.schema import AbstractRespVo
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ def gen_vo_from_exception(
16
+ vo_cls: type(AbstractRespVo),
17
+ e: Exception,
18
+ api_name: Optional[str] = None,
19
+ request_id: Optional[str] = None,
20
+ api_id: Optional[BasicApiId] = None,
21
+ ) -> AbstractRespVo:
22
+ """
23
+ 将异常转换为vo
24
+ """
25
+
26
+ _params = dict(api_name=api_name, request_id=request_id)
27
+ if isinstance(e, BasicCommonException):
28
+ vo = vo_cls.from_exception(e, **_params)
29
+ elif isinstance(e, BasicApiInnerException):
30
+ vo = vo_cls.from_exception(e.add_api_id(api_id=api_id), **_params)
31
+ else:
32
+ vo = vo_cls.from_exception(
33
+ BasicCommonException(CommonStatusCode.UNKNOWN_ERROR, msg=str(e)),
34
+ **_params,
35
+ )
36
+
37
+ return vo
@@ -0,0 +1,71 @@
1
+ import logging
2
+ import time
3
+ import traceback
4
+ from functools import wraps
5
+ from typing import Awaitable, Callable, get_type_hints
6
+
7
+ from algo_backend.exception import (
8
+ BasicApiId,
9
+ DefaultApiErrorCode,
10
+ )
11
+ from algo_backend.metrics import PrometheusTimeCostMetricSetting
12
+ from algo_backend.schema import AbstractRespVo
13
+ from .exception_to_vo import gen_vo_from_exception
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ def timing_and_exception_handler(
19
+ func=None,
20
+ *,
21
+ api_id: BasicApiId = DefaultApiErrorCode.DEFAULT_ERROR,
22
+ api_name: str = "",
23
+ ):
24
+ """
25
+ 装饰器:用于统计函数执行时间并捕获异常
26
+ 函数中需要包含参数reqid或者request_id
27
+ """
28
+
29
+ def decorator(
30
+ func: Callable[..., Awaitable[AbstractRespVo]],
31
+ ) -> Callable[..., Awaitable[AbstractRespVo]]:
32
+ @wraps(func)
33
+ async def wrapper(*args, **kwargs):
34
+ start_time = time.perf_counter()
35
+
36
+ request_id = kwargs.get("request_id", None) or kwargs.get("reqid", None)
37
+ _name = func.__name__ or api_name
38
+
39
+ try:
40
+ # 执行原函数
41
+ logger.info(f"ReqId: {request_id} | Function: {_name} | Start")
42
+ result: AbstractRespVo = await func(*args, **kwargs)
43
+ # 计算耗时
44
+ elapsed_time = time.perf_counter() - start_time
45
+ logger.info(
46
+ f"ReqId: {request_id} | Function: {_name} | COST:{elapsed_time:.4f}s"
47
+ )
48
+ PrometheusTimeCostMetricSetting.api_metrics_instance().add(
49
+ _name, elapsed_time
50
+ )
51
+ return result.set_request_id(request_id)
52
+ except Exception as e:
53
+ # 计算耗时
54
+ elapsed_time = time.perf_counter() - start_time
55
+ PrometheusTimeCostMetricSetting.api_metrics_instance().add_error(_name)
56
+ # 记录异常信息和完整堆栈
57
+ logger.error(
58
+ f"ReqId: {request_id} | Function: {_name} | COST:{elapsed_time:.4f}s | Exception: {str(e)}\n"
59
+ f"Traceback:\n{traceback.format_exc()}"
60
+ )
61
+
62
+ vo_cls: type(AbstractRespVo) = get_type_hints(func).get(
63
+ "return"
64
+ ) # 这里可能会失败,因为无法强制用户的类型
65
+ return gen_vo_from_exception(
66
+ vo_cls, e, api_name=_name, request_id=request_id, api_id=api_id
67
+ )
68
+
69
+ return wrapper
70
+
71
+ return decorator if func is None else decorator(func)
@@ -0,0 +1,9 @@
1
+ from .common import BasicExceptionInterceptor
2
+ from .http import HTTPExceptionInterceptor
3
+ from .validate import ValidateExceptionInterceptor
4
+
5
+ __all__ = [
6
+ "HTTPExceptionInterceptor",
7
+ "ValidateExceptionInterceptor",
8
+ "BasicExceptionInterceptor",
9
+ ]
@@ -0,0 +1,45 @@
1
+ from abc import ABC, abstractmethod
2
+ from typing import Dict, Optional
3
+
4
+ from starlette.requests import Request
5
+
6
+ from algo_backend.schema import AbstractRespVo, BaseRespVo
7
+ from algo_backend.utils.utils import gen_random_request_id
8
+
9
+
10
+ class BasicExceptionInterceptor(ABC):
11
+ """
12
+ 拦截接口关于schema检验的报错,并返回约定的body
13
+ """
14
+
15
+ def __init__(
16
+ self,
17
+ default_vo_type: type(AbstractRespVo) = BaseRespVo,
18
+ url_vo_dict: Optional[Dict[str, type(AbstractRespVo)]] = None,
19
+ ):
20
+ self.default_vo_type: type(AbstractRespVo) = default_vo_type
21
+ # url和响应的匹配策略
22
+ self.url_vo_dict = url_vo_dict or {}
23
+
24
+ def get_vo_type(self, url: str) -> type(AbstractRespVo):
25
+ """
26
+ 用户可以继承BasicExceptionInterceptor的子类,覆盖这个方法从而实现更加复杂的url和响应体匹配策略
27
+ :param url:
28
+ :return:
29
+ """
30
+ return self.url_vo_dict.get(url, self.default_vo_type)
31
+
32
+ @classmethod
33
+ def extract_url(cls, request: Request):
34
+ try:
35
+ url = request.url.path
36
+ except Exception:
37
+ url = ""
38
+ return url
39
+
40
+ @classmethod
41
+ def get_request_id(cls, request: Request):
42
+ return request.headers.get("x-request-id", gen_random_request_id())
43
+
44
+ @abstractmethod
45
+ def intercept(self, request: Request, exc: Exception): ...
@@ -0,0 +1,40 @@
1
+ import logging
2
+
3
+ from fastapi.exceptions import StarletteHTTPException
4
+ from fastapi.responses import JSONResponse
5
+ from starlette.requests import Request
6
+
7
+ from algo_backend.exception import BasicCommonException, CommonStatusCode
8
+
9
+ from .common import BasicExceptionInterceptor
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class HTTPExceptionInterceptor(BasicExceptionInterceptor):
15
+ """
16
+ 拦截接口关于http异常的报错,并返回约定的body
17
+ """
18
+
19
+ def __init__(self, **kwargs):
20
+ super().__init__(**kwargs)
21
+
22
+ def intercept(self, request: Request, exc: StarletteHTTPException):
23
+ url: str = self.extract_url(request)
24
+ reqid: str = self.get_request_id(request)
25
+
26
+ logger.info(
27
+ f"req_id: {url} | 错误码: {exc.status_code} | 错误信息: {exc.detail}"
28
+ )
29
+ if exc.status_code == 405:
30
+ e = BasicCommonException(CommonStatusCode.ERROR_REQ_METHOD, url=url)
31
+ elif exc.status_code == 404:
32
+ e = BasicCommonException(CommonStatusCode.ERROR_REQ_URL, url=url)
33
+ else:
34
+ e = BasicCommonException(
35
+ CommonStatusCode.UNKNOWN_ERROR, msg=f"{exc.status_code},{exc.detail}"
36
+ )
37
+
38
+ vo = self.get_vo_type(url).from_exception(e, request_id=reqid)
39
+
40
+ return JSONResponse(content=vo.model_dump(), status_code=200)
@@ -0,0 +1,78 @@
1
+ import logging
2
+
3
+ from fastapi.exceptions import RequestValidationError
4
+ from fastapi.responses import JSONResponse
5
+ from starlette.requests import Request
6
+
7
+ from algo_backend.exception import BasicCommonException, CommonStatusCode
8
+
9
+ from .common import BasicExceptionInterceptor
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ class ValidateExceptionInterceptor(BasicExceptionInterceptor):
15
+ """
16
+ 拦截接口关于schema检验的报错,并返回约定的body
17
+ """
18
+
19
+ def __init__(self, exc_msg_len=None, **kwargs):
20
+ super().__init__(**kwargs)
21
+ self.exc_msg_len: int = exc_msg_len or 600
22
+
23
+ def intercept(self, request: Request, exc: RequestValidationError) -> JSONResponse:
24
+ """
25
+ 拦截接口关于schema检验的报错,并返回约定的body
26
+ """
27
+
28
+ reqid: str = self.get_request_id(request)
29
+ url: str = self.extract_url(request)
30
+
31
+ logger.error(
32
+ f"Reqid: {reqid} | 请求体参数错误: {str(exc)[: self.exc_msg_len]}..."
33
+ )
34
+ if int(request.headers.get("content-length")) == 0 and request.method in (
35
+ "POST",
36
+ "PUT",
37
+ "PATCH",
38
+ ):
39
+ e = BasicCommonException(
40
+ CommonStatusCode.BODY_EMPTY_ERR,
41
+ url=url,
42
+ )
43
+ else:
44
+ reason: str = self.parse_request_validation_error(exc)
45
+ e = BasicCommonException(
46
+ CommonStatusCode.PARAM_ERROR,
47
+ url=url,
48
+ msg=reason,
49
+ )
50
+
51
+ vo = self.get_vo_type(url).from_exception(e, request_id=reqid)
52
+
53
+ return JSONResponse(
54
+ status_code=200, # 或其他合适的 HTTP 状态码
55
+ content=vo.model_dump(), # 将 Pydantic 模型转换为字典
56
+ )
57
+
58
+ @staticmethod
59
+ def parse_request_validation_error(exc: RequestValidationError):
60
+ """
61
+ 报错润色
62
+ RequestValidationError([{'type': 'string_type', 'loc': ('body', 'messages', 0, 'role'), 'msg': 'Input should be a valid string...[]},
63
+ {'type': 'string_type', 'loc': ('body', 'messages', 0, 'content'), 'msg': 'Input should be a valid string', 'input': []}])
64
+ 转换结果如下:
65
+ '请求参数错误,发现2个字段问题:messages[0].role: Input should be a valid string;messages[0].content: Input should be a valid string'
66
+ """
67
+ error_fields = []
68
+ for error in exc.errors():
69
+ loc = error["loc"]
70
+ # 忽略第一个元素(通常是 body)
71
+ field_path = ".".join(
72
+ f"[{i}]" if isinstance(i, int) else i for i in loc[1:]
73
+ ).replace(".[", "[")
74
+ msg = error["msg"]
75
+ error_fields.append(f"{field_path}: {msg}")
76
+ return f"请求参数错误,发现{len(error_fields)}个字段问题:" + ";".join(
77
+ error_fields
78
+ )
@@ -0,0 +1 @@
1
+ from .common import BasicLogStarter
@@ -0,0 +1,16 @@
1
+ class BasicLogStarter:
2
+ """
3
+ 日志启动器,初始化日志格式和落盘等
4
+ """
5
+
6
+ def __init__(self, service_name: str):
7
+ """
8
+ :param service_name: 服务名
9
+ """
10
+ self.service_name = service_name
11
+
12
+ def setup_log(self):
13
+ """
14
+ 执行一些日志设置
15
+ """
16
+ pass
@@ -0,0 +1,5 @@
1
+ from .log_clean import LoguruCleaner
2
+ from .log_setup import LoguruSetup
3
+ from .log_starter import LoguruStarter
4
+
5
+ __all__ = ["LoguruCleaner", "LoguruSetup", "LoguruStarter"]