init: 补充全局异常处理、基础接口实现

main
chenzhirong 5 months ago
parent 61eea6dc63
commit 51eb18cd9f

@ -15,6 +15,8 @@ from config import settings
__all__ = ["app"]
from exceptions.global_exc import configure_exception
from middleware import add_middleware
@ -52,14 +54,14 @@ def custom_openapi():
routes=app.routes,
)
# # 添加全局安全方案
# openapi_schema["components"]["securitySchemes"] = {
# "global_auth": {
# "type": "apiKey",
# "in": "header",
# "name": "Authorization" # 这里可以改为任何需要的请求头名称
# }
# }
# 添加全局安全方案
openapi_schema["components"]["securitySchemes"] = {
"global_auth": {
"type": "apiKey",
"in": "header",
"name": "Authorization" # 这里可以改为任何需要的请求头名称
}
}
# 应用全局安全要求
openapi_schema["security"] = [
@ -73,7 +75,7 @@ def custom_openapi():
app.openapi = custom_openapi
# 全局异常处理
# configure_exception(app)
configure_exception(app)
# 中间件
add_middleware(app=app)
white_list = ["/docs", "/openapi.json", "/redoc"]

@ -0,0 +1,82 @@
"""全局异常处理
"""
import logging
__all__ = ['AppException']
from enum import IntEnum, Enum
logger = logging.getLogger(__name__)
class CustomEnum(Enum):
@classmethod
def valid(cls, value):
try:
cls(value)
return True
except BaseException:
return False
@classmethod
def values(cls):
return [member.value for member in cls.__members__.values()]
@classmethod
def names(cls):
return [member.name for member in cls.__members__.values()]
class RetCode(IntEnum, CustomEnum):
"""
SUCCESS = 0 # 成功
NOT_EFFECTIVE = 10 # 未生效
EXCEPTION_ERROR = 100 # 异常错误
ARGUMENT_ERROR = 101 # 参数错误
DATA_ERROR = 102 # 数据错误
OPERATING_ERROR = 103 # 操作错误
CONNECTION_ERROR = 105 # 连接错误
RUNNING = 106 # 运行中
PERMISSION_ERROR = 108 # 权限错误
AUTHENTICATION_ERROR = 109 # 认证错误
UNAUTHORIZED = 401 # 未授权
SERVER_ERROR = 500 # 服务器错误
FORBIDDEN = 403 # 禁止访问
NOT_FOUND = 404 # 未找到
"""
SUCCESS = 0 # 成功
NOT_EFFECTIVE = 10 # 未生效
EXCEPTION_ERROR = 100 # 异常错误
ARGUMENT_ERROR = 101 # 参数错误
DATA_ERROR = 102 # 数据错误
OPERATING_ERROR = 103 # 操作错误
CONNECTION_ERROR = 105 # 连接错误
RUNNING = 106 # 运行中
PERMISSION_ERROR = 108 # 权限错误
AUTHENTICATION_ERROR = 109 # 认证错误
UNAUTHORIZED = 401 # 未授权
SERVER_ERROR = 500 # 服务器错误
FORBIDDEN = 403 # 禁止访问
NOT_FOUND = 404 # 未找到
class AppException(Exception):
"""应用异常基类
"""
def __init__(self, msg, *args, code: int = None, echo_exc: bool = False, **kwargs):
super().__init__()
self._code = RetCode.SERVER_ERROR.value if code is None else code
self._message = msg
self.echo_exc = echo_exc
self.args = args or []
self.kwargs = kwargs or {}
@property
def code(self) -> int:
return self._code
@property
def msg(self) -> str:
return self._message
def __str__(self):
return '{}: {}'.format(self.code, self.msg)

@ -0,0 +1,38 @@
import logging
from fastapi import FastAPI, Request
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from utils.api_utils import server_error_response
from .base import AppException
logger = logging.getLogger(__name__)
def configure_exception(app: FastAPI):
"""配置全局异常处理
"""
@app.exception_handler(AppException)
async def app_exception_handler(request: Request, exc: AppException):
"""处理自定义异常
code: .
"""
if exc.echo_exc:
logging.error('app_exception_handler: url=[%s]', request.url.path)
logging.error(exc, exc_info=True)
return JSONResponse(
status_code=200,
content={'code': exc.code, 'message': exc.msg, 'data': False})
@app.exception_handler(RequestValidationError)
async def validation_exception_handler(request: Request, exc: RequestValidationError):
return JSONResponse(
status_code=422,
content={"detail": exc.errors(), "body": exc.body}
)
@app.exception_handler(Exception)
async def global_exception_handler(request: Request, exc: Exception):
return server_error_response(exc)

@ -21,6 +21,7 @@ dependencies = [
"aiomysql>=0.2.0",
"beartype>=0.21.0",
"fastapi-pagination>=0.14.1",
"psutil>=7.1.0",
]
[[tool.uv.index]]
url = "https://mirrors.aliyun.com/pypi/simple"

@ -0,0 +1,83 @@
import logging
from typing import Union, Type
from pydantic import BaseModel
from exceptions.base import AppException
from utils import get_uuid
class BaseController:
def __init__(self, service):
self.service = service
async def base_page(self, req: Union[dict, BaseModel], dto_class: Type[BaseModel] = None):
if not isinstance(req, dict):
req = req.model_dump()
result = await self.service.get_by_page(req)
datas = result.data
if datas and dto_class:
result.data = self.service.entity_conversion_dto(datas, dto_class)
return result
async def base_list(self, req: Union[dict, BaseModel], dto_class: Type[BaseModel] = None):
if not isinstance(req, dict):
req = req.model_dump()
datas = await self.service.get_list(req)
if datas and dto_class:
datas = self.service.entity_conversion_dto(datas, dto_class)
return datas
async def get_all(self, dto_class: Type[BaseModel] = None):
result = await self.service.get_all()
if dto_class:
result = self.service.entity_conversion_dto(result, dto_class)
return result
async def get_by_id(self, id: str, dto_class: Type[BaseModel] = None):
data = await self.service.get_by_id(id)
if not data:
raise AppException(f"不存在 id 为{id}的数据")
result = data.to_dict()
if dto_class:
result = self.service.entity_conversion_dto(result, dto_class)
return result
async def add(self, req: Union[dict, BaseModel]):
if not isinstance(req, dict):
req = req.model_dump()
req["id"] = get_uuid()
try:
return await self.service.save(**req)
except Exception as e:
logging.exception(e)
raise AppException(f"添加失败, error: {str(e)}")
async def delete(self, id: str, db_query_data=None):
if db_query_data is None:
db_query_data = await self.service.get_by_id(id)
if not db_query_data:
raise AppException(f"数据不存在")
self.service.check_base_permission(db_query_data)
try:
return await self.service.delete_by_id(id)
except Exception as e:
logging.exception(e)
raise AppException(f"删除失败")
async def update(self, request: BaseModel, db_query_data=None):
params = request.model_dump()
req = {k: v for k, v in params.items() if v is not None}
data_id = req.get("id")
if db_query_data is None:
db_query_data = await self.service.get_by_id(data_id)
if not db_query_data:
raise AppException(f"数据不存在")
self.service.check_base_permission(db_query_data)
try:
return await self.service.update_by_id(data_id, req)
except Exception as e:
logging.exception(e)
raise AppException(f"更新失败, error: {str(e)}")

@ -0,0 +1,21 @@
import logging
from fastapi import APIRouter, Depends
from server_info import ServerInfo
logger = logging.getLogger(__name__)
router = APIRouter(prefix='/monitor', tags=["缓存监控服务"])
@router.get('/server',summary='服务监控')
def monitor_server():
"""服务器信息监控"""
return {
'cpu': ServerInfo.get_cpu_info(),
'mem': ServerInfo.get_mem_info(),
'sys': ServerInfo.get_sys_info(),
'disk': ServerInfo.get_disk_info(),
'py': ServerInfo.get_py_info(),
}

@ -0,0 +1,122 @@
import os
import platform
import sys
from datetime import datetime, timedelta
from typing import List, Any
import psutil
from config import get_settings
from utils.ip_utils import IpUtil
def get_attr(obj: Any, attr: str, default: Any = None) -> Any:
return getattr(obj, attr, default)
class ServerInfo:
"""服务器相关信息"""
datetime_fmt = get_settings().datetime_fmt
@staticmethod
def get_size(data, suffix='B') -> str:
"""
按照正确的格式缩放字节
eg:
1253656 => '1.20MB'
1253656678 => '1.17GB'
"""
factor = 1024
for unit in ['', 'K', 'M', 'G', 'T', 'P']:
if data < factor:
return f'{data:.2f}{unit}{suffix}'
data /= factor
@staticmethod
def fmt_timedelta(td: timedelta) -> str:
"""格式化显示timedelta
eg:
timedelta => xx天xx小时xx分钟
"""
rem = td.seconds
days, rem = rem // 86400, rem % 86400
hours, rem = rem // 3600, rem % 3600
minutes = rem // 60
res = f'{minutes}分钟'
if hours > 0:
res = f'{hours}小时{res}'
if days > 0:
res = f'{days}{res}'
return res
@staticmethod
def get_cpu_info() -> dict:
"""获取CPU信息"""
res = {'cpu_num': psutil.cpu_count(logical=True)}
cpu_times = psutil.cpu_times()
total = cpu_times.user + cpu_times.nice + cpu_times.system + cpu_times.idle \
+ get_attr(cpu_times, 'iowait', 0.0) + get_attr(cpu_times, 'irq', 0.0) \
+ get_attr(cpu_times, 'softirq', 0.0) + get_attr(cpu_times, 'steal', 0.0)
res['total'] = round(total, 2)
res['sys'] = round(cpu_times.system / total, 2)
res['used'] = round(cpu_times.user / total, 2)
res['wait'] = round(get_attr(cpu_times, 'iowait', 0.0) / total, 2)
res['free'] = round(cpu_times.idle / total, 2)
return res
@staticmethod
def get_mem_info() -> dict:
"""获取内存信息"""
number = 1024 ** 3
return {
'total': round(psutil.virtual_memory().total / number, 2),
'used': round(psutil.virtual_memory().used / number, 2),
'free': round(psutil.virtual_memory().available / number, 2),
'usage': round(psutil.virtual_memory().percent, 2)}
@staticmethod
def get_sys_info() -> dict:
"""获取服务器信息"""
return {
'computerName': IpUtil.get_host_name(),
'computerIp': IpUtil.get_host_ip(),
'userDir': os.path.dirname(os.path.abspath(os.path.join(__file__, '../..'))),
'osName': platform.system(),
'osArch': platform.machine()}
@staticmethod
def get_disk_info() -> List[dict]:
"""获取磁盘信息"""
disk_info = []
for disk in psutil.disk_partitions():
usage = psutil.disk_usage(disk.mountpoint)
disk_info.append({
'dirName': disk.mountpoint,
'sysTypeName': disk.fstype,
'typeName': disk.device,
'total': ServerInfo.get_size(usage.total),
'free': ServerInfo.get_size(usage.free),
'used': ServerInfo.get_size(usage.used),
'usage': round(usage.percent, 2),
})
return disk_info
@staticmethod
def get_py_info():
"""获取Python环境及服务信息"""
number = 1024 ** 2
cur_proc = psutil.Process(os.getpid())
mem_info = cur_proc.memory_info()
start_dt = datetime.fromtimestamp(cur_proc.create_time())
return {
'name': 'Python',
'version': platform.python_version(),
'home': sys.executable,
'inputArgs': '[{}]'.format(', '.join(sys.argv[1:])),
'total': round(mem_info.vms / number, 2),
'max': round(mem_info.vms / number, 2),
'free': round((mem_info.vms - mem_info.rss) / number, 2),
'usage': round(mem_info.rss / number, 2),
'runTime': ServerInfo.fmt_timedelta(datetime.now() - start_dt),
'startTime': start_dt.strftime(ServerInfo.datetime_fmt),
}

@ -8,7 +8,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
from core.global_context import current_session
from entity.dto.base import BasePageQueryReq, BasePageResp, BaseQueryReq
from utils import get_uuid, current_timestamp
from utils import get_uuid
"""
session.execute 执行任意数据库操作语句返回结果需要额外处理获取 数据形式Row对象类似元组
@ -18,6 +18,8 @@ session.scalars: 只适合单模型查询(不适合指定列或连表查询)
session.scalar: 直接明确获取一条数据可以直接返回无需额外处理
"""
class BaseService:
model = None # 子类必须指定模型
@ -29,6 +31,7 @@ class BaseService:
raise RuntimeError("No database session in context. "
"Make sure to use this service within a request context.")
return session
@classmethod
def get_query_stmt(cls, query_params, sessions=None, *, fields: list = None):
if sessions is None:
@ -41,6 +44,7 @@ class BaseService:
field = getattr(cls.model, key)
sessions = sessions.where(field == value)
return sessions
@classmethod
def entity_conversion_dto(cls, entity_data: Union[list, BaseModel], dto: Type[BaseModel]) -> Union[
BaseModel, List[BaseModel]]:
@ -54,6 +58,11 @@ class BaseService:
dto_list.append(dto(**temp))
return dto_list
@classmethod
def check_base_permission(cls, daba: Any):
# todo
pass
@classmethod
async def get_by_page(cls, query_params: Union[dict, BasePageQueryReq]):
if not isinstance(query_params, dict):
@ -210,7 +219,6 @@ class BaseService:
session = cls.get_db()
return await session.scalar(stmt)
@classmethod
async def is_exist(cls, query_params: dict = None):
return await cls.get_data_count(query_params) > 0

@ -0,0 +1,26 @@
import logging
import socket
logger = logging.getLogger(__name__)
class IpUtil:
"""IP工具类"""
@staticmethod
def get_host_name() -> str:
"""获取本地IP地址"""
return socket.gethostname()
@staticmethod
def get_host_ip() -> str:
"""获取本地主机名"""
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
try:
s.connect(('10.0.0.0', 0))
ip = s.getsockname()[0]
except Exception as _:
ip = '127.0.0.1'
finally:
s.close()
return ip

@ -736,6 +736,22 @@ wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759" },
]
[[package]]
name = "psutil"
version = "7.1.0"
source = { registry = "https://mirrors.aliyun.com/pypi/simple" }
sdist = { url = "https://mirrors.aliyun.com/pypi/packages/b3/31/4723d756b59344b643542936e37a31d1d3204bcdc42a7daa8ee9eb06fb50/psutil-7.1.0.tar.gz", hash = "sha256:655708b3c069387c8b77b072fc429a57d0e214221d01c0a772df7dfedcb3bcd2" }
wheels = [
{ url = "https://mirrors.aliyun.com/pypi/packages/46/62/ce4051019ee20ce0ed74432dd73a5bb087a6704284a470bb8adff69a0932/psutil-7.1.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:76168cef4397494250e9f4e73eb3752b146de1dd950040b29186d0cce1d5ca13" },
{ url = "https://mirrors.aliyun.com/pypi/packages/38/61/f76959fba841bf5b61123fbf4b650886dc4094c6858008b5bf73d9057216/psutil-7.1.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:5d007560c8c372efdff9e4579c2846d71de737e4605f611437255e81efcca2c5" },
{ url = "https://mirrors.aliyun.com/pypi/packages/88/7a/37c99d2e77ec30d63398ffa6a660450b8a62517cabe44b3e9bae97696e8d/psutil-7.1.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:22e4454970b32472ce7deaa45d045b34d3648ce478e26a04c7e858a0a6e75ff3" },
{ url = "https://mirrors.aliyun.com/pypi/packages/9d/de/04c8c61232f7244aa0a4b9a9fbd63a89d5aeaf94b2fc9d1d16e2faa5cbb0/psutil-7.1.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8c70e113920d51e89f212dd7be06219a9b88014e63a4cec69b684c327bc474e3" },
{ url = "https://mirrors.aliyun.com/pypi/packages/f4/58/c4f976234bf6d4737bc8c02a81192f045c307b72cf39c9e5c5a2d78927f6/psutil-7.1.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7d4a113425c037300de3ac8b331637293da9be9713855c4fc9d2d97436d7259d" },
{ url = "https://mirrors.aliyun.com/pypi/packages/79/87/157c8e7959ec39ced1b11cc93c730c4fb7f9d408569a6c59dbd92ceb35db/psutil-7.1.0-cp37-abi3-win32.whl", hash = "sha256:09ad740870c8d219ed8daae0ad3b726d3bf9a028a198e7f3080f6a1888b99bca" },
{ url = "https://mirrors.aliyun.com/pypi/packages/bf/e9/b44c4f697276a7a95b8e94d0e320a7bf7f3318521b23de69035540b39838/psutil-7.1.0-cp37-abi3-win_amd64.whl", hash = "sha256:57f5e987c36d3146c0dd2528cd42151cf96cd359b9d67cfff836995cc5df9a3d" },
{ url = "https://mirrors.aliyun.com/pypi/packages/26/65/1070a6e3c036f39142c2820c4b52e9243246fcfc3f96239ac84472ba361e/psutil-7.1.0-cp37-abi3-win_arm64.whl", hash = "sha256:6937cb68133e7c97b6cc9649a570c9a18ba0efebed46d8c5dae4c07fa1b67a07" },
]
[[package]]
name = "pydantic"
version = "2.11.7"
@ -1027,6 +1043,7 @@ dependencies = [
{ name = "langchainhub" },
{ name = "mcp" },
{ name = "openai" },
{ name = "psutil" },
{ name = "ruamel-yaml" },
{ name = "sqlalchemy", extra = ["asyncio"] },
{ name = "sqlmodel" },
@ -1047,6 +1064,7 @@ requires-dist = [
{ name = "langchainhub", specifier = ">=0.1.21" },
{ name = "mcp", specifier = ">=1.14.0" },
{ name = "openai", specifier = ">=1.107.3" },
{ name = "psutil", specifier = ">=7.1.0" },
{ name = "ruamel-yaml", specifier = ">=0.18.6,<0.19.0" },
{ name = "sqlalchemy", extras = ["asyncio"], specifier = ">=2.0.43" },
{ name = "sqlmodel", specifier = ">=0.0.25" },

Loading…
Cancel
Save