mirror of
https://github.com/fastapi-practices/fastapi-best-architecture.git
synced 2026-09-21 21:15:13 +00:00
Optimize codes and comments with cursor (#550)
This commit is contained in:
+17
-12
@@ -7,7 +7,12 @@ from backend.utils.serializers import RowData, select_list_serialize
|
||||
|
||||
|
||||
def get_tree_nodes(row: Sequence[RowData]) -> list[dict[str, Any]]:
|
||||
"""获取所有树形结构节点"""
|
||||
"""
|
||||
获取所有树形结构节点
|
||||
|
||||
:param row: 原始数据行序列
|
||||
:return:
|
||||
"""
|
||||
tree_nodes = select_list_serialize(row)
|
||||
tree_nodes.sort(key=lambda x: x['sort'])
|
||||
return tree_nodes
|
||||
@@ -17,10 +22,10 @@ def traversal_to_tree(nodes: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||
"""
|
||||
通过遍历算法构造树形结构
|
||||
|
||||
:param nodes:
|
||||
:param nodes: 树节点列表
|
||||
:return:
|
||||
"""
|
||||
tree = []
|
||||
tree: list[dict[str, Any]] = []
|
||||
node_dict = {node['id']: node for node in nodes}
|
||||
|
||||
for node in nodes:
|
||||
@@ -45,16 +50,16 @@ def recursive_to_tree(nodes: list[dict[str, Any]], *, parent_id: int | None = No
|
||||
"""
|
||||
通过递归算法构造树形结构(性能影响较大)
|
||||
|
||||
:param nodes:
|
||||
:param parent_id:
|
||||
:param nodes: 树节点列表
|
||||
:param parent_id: 父节点 ID,默认为 None 表示根节点
|
||||
:return:
|
||||
"""
|
||||
tree = []
|
||||
tree: list[dict[str, Any]] = []
|
||||
for node in nodes:
|
||||
if node['parent_id'] == parent_id:
|
||||
child_node = recursive_to_tree(nodes, parent_id=node['id'])
|
||||
if child_node:
|
||||
node['children'] = child_node
|
||||
child_nodes = recursive_to_tree(nodes, parent_id=node['id'])
|
||||
if child_nodes:
|
||||
node['children'] = child_nodes
|
||||
tree.append(node)
|
||||
return tree
|
||||
|
||||
@@ -65,9 +70,9 @@ def get_tree_data(
|
||||
"""
|
||||
获取树形结构数据
|
||||
|
||||
:param row:
|
||||
:param build_type:
|
||||
:param parent_id:
|
||||
:param row: 原始数据行序列
|
||||
:param build_type: 构建树形结构的算法类型,默认为遍历算法
|
||||
:param parent_id: 父节点 ID,仅在递归算法中使用
|
||||
:return:
|
||||
"""
|
||||
nodes = get_tree_nodes(row)
|
||||
|
||||
@@ -6,9 +6,13 @@ from backend.common.exception import errors
|
||||
from backend.core.conf import settings
|
||||
|
||||
|
||||
async def demo_site(request: Request):
|
||||
"""演示站点"""
|
||||
async def demo_site(request: Request) -> None:
|
||||
"""
|
||||
演示站点
|
||||
|
||||
:param request: FastAPI 请求对象
|
||||
:return:
|
||||
"""
|
||||
method = request.method
|
||||
path = request.url.path
|
||||
if (
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import hashlib
|
||||
import os
|
||||
|
||||
from typing import Any
|
||||
@@ -13,9 +14,14 @@ from backend.common.log import log
|
||||
|
||||
|
||||
class AESCipher:
|
||||
def __init__(self, key: bytes | str):
|
||||
"""AES 加密器"""
|
||||
|
||||
def __init__(self, key: bytes | str) -> None:
|
||||
"""
|
||||
初始化 AES 加密器
|
||||
|
||||
:param key: 密钥,16/24/32 bytes 或 16 进制字符串
|
||||
:return:
|
||||
"""
|
||||
self.key = key if isinstance(key, bytes) else bytes.fromhex(key)
|
||||
|
||||
@@ -40,7 +46,7 @@ class AESCipher:
|
||||
"""
|
||||
AES 解密
|
||||
|
||||
:param ciphertext: 解密前的密文, bytes 或 16 进制字符串
|
||||
:param ciphertext: 解密前的密文,bytes 或 16 进制字符串
|
||||
:return:
|
||||
"""
|
||||
ciphertext = ciphertext if isinstance(ciphertext, bytes) else bytes.fromhex(ciphertext)
|
||||
@@ -55,6 +61,8 @@ class AESCipher:
|
||||
|
||||
|
||||
class Md5Cipher:
|
||||
"""MD5 加密器"""
|
||||
|
||||
@staticmethod
|
||||
def encrypt(plaintext: bytes | str) -> str:
|
||||
"""
|
||||
@@ -63,8 +71,6 @@ class Md5Cipher:
|
||||
:param plaintext: 加密前的明文
|
||||
:return:
|
||||
"""
|
||||
import hashlib
|
||||
|
||||
md5 = hashlib.md5()
|
||||
if not isinstance(plaintext, bytes):
|
||||
plaintext = str(plaintext).encode('utf-8')
|
||||
@@ -73,15 +79,20 @@ class Md5Cipher:
|
||||
|
||||
|
||||
class ItsDCipher:
|
||||
def __init__(self, key: bytes | str):
|
||||
"""ItsDangerous 加密器"""
|
||||
|
||||
def __init__(self, key: bytes | str) -> None:
|
||||
"""
|
||||
初始化 ItsDangerous 加密器
|
||||
|
||||
:param key: 密钥,16/24/32 bytes 或 16 进制字符串
|
||||
:return:
|
||||
"""
|
||||
self.key = key if isinstance(key, bytes) else bytes.fromhex(key)
|
||||
|
||||
def encrypt(self, plaintext: Any) -> str:
|
||||
"""
|
||||
ItsDangerous 加密 (可能失败,如果 plaintext 无法序列化,则会加密为 MD5)
|
||||
ItsDangerous 加密
|
||||
|
||||
:param plaintext: 加密前的明文
|
||||
:return:
|
||||
@@ -96,7 +107,7 @@ class ItsDCipher:
|
||||
|
||||
def decrypt(self, ciphertext: str) -> Any:
|
||||
"""
|
||||
ItsDangerous 解密 (可能失败,如果 ciphertext 无法反序列化,则解密失败, 返回原始密文)
|
||||
ItsDangerous 解密
|
||||
|
||||
:param ciphertext: 解密前的密文
|
||||
:return:
|
||||
|
||||
@@ -14,11 +14,11 @@ from backend.core.path_conf import UPLOAD_DIR
|
||||
from backend.utils.timezone import timezone
|
||||
|
||||
|
||||
def build_filename(file: UploadFile):
|
||||
def build_filename(file: UploadFile) -> str:
|
||||
"""
|
||||
构建文件名
|
||||
|
||||
:param file:
|
||||
:param file: FastAPI 上传文件对象
|
||||
:return:
|
||||
"""
|
||||
timestamp = int(timezone.now().timestamp())
|
||||
@@ -32,14 +32,15 @@ def file_verify(file: UploadFile, file_type: FileType) -> None:
|
||||
"""
|
||||
文件验证
|
||||
|
||||
:param file:
|
||||
:param file_type:
|
||||
:param file: FastAPI 上传文件对象
|
||||
:param file_type: 文件类型枚举
|
||||
:return:
|
||||
"""
|
||||
filename = file.filename
|
||||
file_ext = filename.split('.')[-1].lower()
|
||||
if not file_ext:
|
||||
raise errors.ForbiddenError(msg='未知的文件类型')
|
||||
|
||||
if file_type == FileType.image:
|
||||
if file_ext not in settings.UPLOAD_IMAGE_EXT_INCLUDE:
|
||||
raise errors.ForbiddenError(msg='此图片格式暂不支持')
|
||||
@@ -52,11 +53,11 @@ def file_verify(file: UploadFile, file_type: FileType) -> None:
|
||||
raise errors.ForbiddenError(msg='视频超出最大限制,请重新选择')
|
||||
|
||||
|
||||
async def upload_file(file: UploadFile):
|
||||
async def upload_file(file: UploadFile) -> str:
|
||||
"""
|
||||
上传文件
|
||||
|
||||
:param file:
|
||||
:param file: FastAPI 上传文件对象
|
||||
:return:
|
||||
"""
|
||||
filename = build_filename(file)
|
||||
|
||||
@@ -12,7 +12,8 @@ from backend.core.path_conf import JINJA2_TEMPLATE_DIR
|
||||
|
||||
|
||||
class GenTemplate:
|
||||
def __init__(self):
|
||||
def __init__(self) -> None:
|
||||
"""初始化模板生成器"""
|
||||
self.env = Environment(
|
||||
loader=FileSystemLoader(JINJA2_TEMPLATE_DIR),
|
||||
autoescape=select_autoescape(enabled_extensions=['jinja']),
|
||||
@@ -25,18 +26,17 @@ class GenTemplate:
|
||||
|
||||
def get_template(self, jinja_file: str) -> Template:
|
||||
"""
|
||||
获取模版文件
|
||||
获取模板文件
|
||||
|
||||
:param jinja_file:
|
||||
:param jinja_file: Jinja2 模板文件
|
||||
:return:
|
||||
"""
|
||||
|
||||
return self.env.get_template(jinja_file)
|
||||
|
||||
@staticmethod
|
||||
def get_template_paths() -> list[str]:
|
||||
"""
|
||||
获取模版文件路径
|
||||
获取模板文件路径列表
|
||||
|
||||
:return:
|
||||
"""
|
||||
@@ -53,26 +53,25 @@ class GenTemplate:
|
||||
"""
|
||||
获取代码生成路径列表
|
||||
|
||||
:param business:
|
||||
:param business: 代码生成业务对象
|
||||
:return:
|
||||
"""
|
||||
app_name = business.app_name
|
||||
module_name = business.table_name_en
|
||||
target_files = [
|
||||
return [
|
||||
f'{generator_settings.TEMPLATE_BACKEND_DIR_NAME}/{app_name}/api/{business.api_version}/{module_name}.py',
|
||||
f'{generator_settings.TEMPLATE_BACKEND_DIR_NAME}/{app_name}/crud/crud_{module_name}.py',
|
||||
f'{generator_settings.TEMPLATE_BACKEND_DIR_NAME}/{app_name}/model/{module_name}.py',
|
||||
f'{generator_settings.TEMPLATE_BACKEND_DIR_NAME}/{app_name}/schema/{module_name}.py',
|
||||
f'{generator_settings.TEMPLATE_BACKEND_DIR_NAME}/{app_name}/service/{module_name}_service.py',
|
||||
]
|
||||
return target_files
|
||||
|
||||
def get_code_gen_path(self, tpl_path: str, business: GenBusiness) -> str:
|
||||
"""
|
||||
获取代码生成路径
|
||||
|
||||
:param tpl_path:
|
||||
:param business:
|
||||
:param tpl_path: 模板文件路径
|
||||
:param business: 代码生成业务对象
|
||||
:return:
|
||||
"""
|
||||
target_files = self.get_code_gen_paths(business)
|
||||
@@ -80,12 +79,12 @@ class GenTemplate:
|
||||
return code_gen_path_mapping[tpl_path]
|
||||
|
||||
@staticmethod
|
||||
def get_vars(business: GenBusiness, models: Sequence[GenModel]) -> dict:
|
||||
def get_vars(business: GenBusiness, models: Sequence[GenModel]) -> dict[str, str | Sequence[GenModel]]:
|
||||
"""
|
||||
获取模版变量
|
||||
获取模板变量
|
||||
|
||||
:param business:
|
||||
:param models:
|
||||
:param business: 代码生成业务对象
|
||||
:param models: 代码生成模型对象列表
|
||||
:return:
|
||||
"""
|
||||
return {
|
||||
|
||||
@@ -12,7 +12,7 @@ def ensure_unique_route_names(app: FastAPI) -> None:
|
||||
"""
|
||||
检查路由名称是否唯一
|
||||
|
||||
:param app:
|
||||
:param app: FastAPI 应用实例
|
||||
:return:
|
||||
"""
|
||||
temp_routes = set()
|
||||
@@ -23,13 +23,13 @@ def ensure_unique_route_names(app: FastAPI) -> None:
|
||||
temp_routes.add(route.name)
|
||||
|
||||
|
||||
async def http_limit_callback(request: Request, response: Response, expire: int):
|
||||
async def http_limit_callback(request: Request, response: Response, expire: int) -> None:
|
||||
"""
|
||||
请求限制时的默认回调函数
|
||||
|
||||
:param request:
|
||||
:param response:
|
||||
:param expire: 剩余毫秒
|
||||
:param request: FastAPI 请求对象
|
||||
:param response: FastAPI 响应对象
|
||||
:param expire: 剩余毫秒数
|
||||
:return:
|
||||
"""
|
||||
expires = ceil(expire / 1000)
|
||||
|
||||
@@ -3,36 +3,36 @@
|
||||
import importlib
|
||||
|
||||
from functools import lru_cache
|
||||
from typing import Any
|
||||
from typing import Any, Type, TypeVar
|
||||
|
||||
from backend.common.exception import errors
|
||||
from backend.common.log import log
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
@lru_cache(maxsize=512)
|
||||
def import_module_cached(module_path: str) -> Any:
|
||||
"""
|
||||
缓存导入模块
|
||||
|
||||
:param module_path:
|
||||
:param module_path: 模块路径
|
||||
:return:
|
||||
"""
|
||||
return importlib.import_module(module_path)
|
||||
|
||||
|
||||
def dynamic_import_data_model(module_path: str) -> Any:
|
||||
def dynamic_import_data_model(module_path: str) -> Type[T]:
|
||||
"""
|
||||
动态导入数据模型
|
||||
|
||||
:param module_path:
|
||||
:param module_path: 模块路径,格式为 'module_path.class_name'
|
||||
:return:
|
||||
"""
|
||||
module_path, class_or_func = module_path.rsplit('.', 1)
|
||||
|
||||
try:
|
||||
module_path, class_name = module_path.rsplit('.', 1)
|
||||
module = import_module_cached(module_path)
|
||||
ins = getattr(module, class_or_func)
|
||||
return getattr(module, class_name)
|
||||
except (ImportError, AttributeError) as e:
|
||||
log.error(e)
|
||||
log.error(f'动态导入数据模型失败:{e}')
|
||||
raise errors.ServerError(msg='数据模型列动态解析失败,请联系系统超级管理员')
|
||||
return ins
|
||||
|
||||
@@ -6,9 +6,9 @@ from fastapi.routing import APIRoute
|
||||
|
||||
def simplify_operation_ids(app: FastAPI) -> None:
|
||||
"""
|
||||
简化操作 ID,以便生成的客户端具有更简单的 api 函数名称
|
||||
简化操作 ID,以便生成的客户端具有更简单的 API 函数名称
|
||||
|
||||
:param app:
|
||||
:param app: FastAPI 应用实例
|
||||
:return:
|
||||
"""
|
||||
for route in app.routes:
|
||||
|
||||
+21
-17
@@ -3,41 +3,45 @@
|
||||
import re
|
||||
|
||||
|
||||
def search_string(pattern, text) -> bool:
|
||||
def search_string(pattern: str, text: str) -> bool:
|
||||
"""
|
||||
全字段正则匹配
|
||||
|
||||
:param pattern:
|
||||
:param text:
|
||||
:param pattern: 正则表达式模式
|
||||
:param text: 待匹配的文本
|
||||
:return:
|
||||
"""
|
||||
result = re.search(pattern, text)
|
||||
if result:
|
||||
return True
|
||||
else:
|
||||
if not pattern or not text:
|
||||
return False
|
||||
|
||||
result = re.search(pattern, text)
|
||||
return result is not None
|
||||
|
||||
def match_string(pattern, text) -> bool:
|
||||
|
||||
def match_string(pattern: str, text: str) -> bool:
|
||||
"""
|
||||
从字段开头正则匹配
|
||||
|
||||
:param pattern:
|
||||
:param text:
|
||||
:param pattern: 正则表达式模式
|
||||
:param text: 待匹配的文本
|
||||
:return:
|
||||
"""
|
||||
result = re.match(pattern, text)
|
||||
if result:
|
||||
return True
|
||||
else:
|
||||
if not pattern or not text:
|
||||
return False
|
||||
|
||||
result = re.match(pattern, text)
|
||||
return result is not None
|
||||
|
||||
|
||||
def is_phone(text: str) -> bool:
|
||||
"""
|
||||
检查手机号码
|
||||
检查手机号码格式
|
||||
|
||||
:param text:
|
||||
:param text: 待检查的手机号码
|
||||
:return:
|
||||
"""
|
||||
return match_string(r'^1[3-9]\d{9}$', text)
|
||||
if not text:
|
||||
return False
|
||||
|
||||
phone_pattern = r'^1[3-9]\d{9}$'
|
||||
return match_string(phone_pattern, text)
|
||||
|
||||
+33
-12
@@ -6,27 +6,48 @@ from backend.utils.server_info import server_info
|
||||
|
||||
class RedisInfo:
|
||||
@staticmethod
|
||||
async def get_info():
|
||||
async def get_info() -> dict[str, str]:
|
||||
"""获取 Redis 服务器信息"""
|
||||
|
||||
# 获取原始信息
|
||||
info = await redis_client.info()
|
||||
fmt_info = {}
|
||||
|
||||
# 格式化信息
|
||||
fmt_info: dict[str, str] = {}
|
||||
for key, value in info.items():
|
||||
if isinstance(value, dict):
|
||||
value = ','.join({f'{k}={v}' for k, v in value.items()})
|
||||
# 将字典格式化为字符串
|
||||
fmt_info[key] = ','.join(f'{k}={v}' for k, v in value.items())
|
||||
else:
|
||||
value = str(value)
|
||||
fmt_info[key] = value
|
||||
fmt_info[key] = str(value)
|
||||
|
||||
# 添加数据库大小信息
|
||||
db_size = await redis_client.dbsize()
|
||||
fmt_info.update({'keys_num': db_size})
|
||||
fmt_uptime = server_info.fmt_seconds(fmt_info.get('uptime_in_seconds', 0))
|
||||
fmt_info.update({'uptime_in_seconds': fmt_uptime})
|
||||
fmt_info['keys_num'] = str(db_size)
|
||||
|
||||
# 格式化运行时间
|
||||
uptime = int(fmt_info.get('uptime_in_seconds', '0'))
|
||||
fmt_info['uptime_in_seconds'] = server_info.fmt_seconds(uptime)
|
||||
|
||||
return fmt_info
|
||||
|
||||
@staticmethod
|
||||
async def get_stats():
|
||||
stats_list = []
|
||||
async def get_stats() -> list[dict[str, str]]:
|
||||
"""获取 Redis 命令统计信息"""
|
||||
|
||||
# 获取命令统计信息
|
||||
command_stats = await redis_client.info('commandstats')
|
||||
for k, v in command_stats.items():
|
||||
stats_list.append({'name': k.split('_')[-1], 'value': str(v.get('calls', ''))})
|
||||
|
||||
# 格式化统计信息
|
||||
stats_list: list[dict[str, str]] = []
|
||||
for key, value in command_stats.items():
|
||||
if not isinstance(value, dict):
|
||||
continue
|
||||
|
||||
command_name = key.split('_')[-1]
|
||||
call_count = str(value.get('calls', '0'))
|
||||
stats_list.append({'name': command_name, 'value': call_count})
|
||||
|
||||
return stats_list
|
||||
|
||||
|
||||
|
||||
@@ -15,28 +15,32 @@ from backend.database.redis import redis_client
|
||||
|
||||
|
||||
def get_request_ip(request: Request) -> str:
|
||||
"""获取请求的 ip 地址"""
|
||||
"""
|
||||
获取请求的 IP 地址
|
||||
|
||||
:param request: FastAPI 请求对象
|
||||
:return:
|
||||
"""
|
||||
real = request.headers.get('X-Real-IP')
|
||||
if real:
|
||||
ip = real
|
||||
else:
|
||||
forwarded = request.headers.get('X-Forwarded-For')
|
||||
if forwarded:
|
||||
ip = forwarded.split(',')[0]
|
||||
else:
|
||||
ip = request.client.host
|
||||
return real
|
||||
|
||||
forwarded = request.headers.get('X-Forwarded-For')
|
||||
if forwarded:
|
||||
return forwarded.split(',')[0]
|
||||
|
||||
# 忽略 pytest
|
||||
if ip == 'testclient':
|
||||
ip = '127.0.0.1'
|
||||
return ip
|
||||
if request.client.host == 'testclient':
|
||||
return '127.0.0.1'
|
||||
return request.client.host
|
||||
|
||||
|
||||
async def get_location_online(ip: str, user_agent: str) -> dict | None:
|
||||
"""
|
||||
在线获取 ip 地址属地,无法保证可用性,准确率较高
|
||||
在线获取 IP 地址属地,无法保证可用性,准确率较高
|
||||
|
||||
:param ip:
|
||||
:param user_agent:
|
||||
:param ip: IP 地址
|
||||
:param user_agent: 用户代理字符串
|
||||
:return:
|
||||
"""
|
||||
async with httpx.AsyncClient(timeout=3) as client:
|
||||
@@ -47,16 +51,16 @@ async def get_location_online(ip: str, user_agent: str) -> dict | None:
|
||||
if response.status_code == 200:
|
||||
return response.json()
|
||||
except Exception as e:
|
||||
log.error(f'在线获取 ip 地址属地失败,错误信息:{e}')
|
||||
log.error(f'在线获取 IP 地址属地失败,错误信息:{e}')
|
||||
return None
|
||||
|
||||
|
||||
@sync_to_async
|
||||
def get_location_offline(ip: str) -> dict | None:
|
||||
"""
|
||||
离线获取 ip 地址属地,无法保证准确率,100%可用
|
||||
离线获取 IP 地址属地,无法保证准确率,100% 可用
|
||||
|
||||
:param ip:
|
||||
:param ip: IP 地址
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
@@ -71,23 +75,30 @@ def get_location_offline(ip: str) -> dict | None:
|
||||
'city': data[3] if data[3] != '0' else None,
|
||||
}
|
||||
except Exception as e:
|
||||
log.error(f'离线获取 ip 地址属地失败,错误信息:{e}')
|
||||
log.error(f'离线获取 IP 地址属地失败,错误信息:{e}')
|
||||
return None
|
||||
|
||||
|
||||
async def parse_ip_info(request: Request) -> IpInfo:
|
||||
"""
|
||||
解析请求的 IP 信息
|
||||
|
||||
:param request: FastAPI 请求对象
|
||||
:return:
|
||||
"""
|
||||
country, region, city = None, None, None
|
||||
ip = get_request_ip(request)
|
||||
location = await redis_client.get(f'{settings.IP_LOCATION_REDIS_PREFIX}:{ip}')
|
||||
if location:
|
||||
country, region, city = location.split('|')
|
||||
return IpInfo(ip=ip, country=country, region=region, city=city)
|
||||
|
||||
location_info = None
|
||||
if settings.IP_LOCATION_PARSE == 'online':
|
||||
location_info = await get_location_online(ip, request.headers.get('User-Agent'))
|
||||
elif settings.IP_LOCATION_PARSE == 'offline':
|
||||
location_info = await get_location_offline(ip)
|
||||
else:
|
||||
location_info = None
|
||||
|
||||
if location_info:
|
||||
country = location_info.get('country')
|
||||
region = location_info.get('regionName')
|
||||
@@ -101,6 +112,12 @@ async def parse_ip_info(request: Request) -> IpInfo:
|
||||
|
||||
|
||||
def parse_user_agent_info(request: Request) -> UserAgentInfo:
|
||||
"""
|
||||
解析请求的用户代理信息
|
||||
|
||||
:param request: FastAPI 请求对象
|
||||
:return:
|
||||
"""
|
||||
user_agent = request.headers.get('User-Agent')
|
||||
_user_agent = parse(user_agent)
|
||||
os = _user_agent.get_os()
|
||||
|
||||
@@ -14,43 +14,38 @@ RowData = Row | RowMapping | Any
|
||||
R = TypeVar('R', bound=RowData)
|
||||
|
||||
|
||||
def select_columns_serialize(row: R) -> dict:
|
||||
def select_columns_serialize(row: R) -> dict[str, Any]:
|
||||
"""
|
||||
Serialize SQLAlchemy select table columns, does not contain relational columns
|
||||
序列化 SQLAlchemy 查询表的列,不包含关联列
|
||||
|
||||
:param row:
|
||||
:param row: SQLAlchemy 查询结果行
|
||||
:return:
|
||||
"""
|
||||
result = {}
|
||||
for column in row.__table__.columns.keys():
|
||||
v = getattr(row, column)
|
||||
if isinstance(v, Decimal):
|
||||
v = decimal_encoder(v)
|
||||
result[column] = v
|
||||
value = getattr(row, column)
|
||||
if isinstance(value, Decimal):
|
||||
value = decimal_encoder(value)
|
||||
result[column] = value
|
||||
return result
|
||||
|
||||
|
||||
def select_list_serialize(row: Sequence[R]) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Serialize SQLAlchemy select list
|
||||
序列化 SQLAlchemy 查询列表
|
||||
|
||||
:param row:
|
||||
:param row: SQLAlchemy 查询结果列表
|
||||
:return:
|
||||
"""
|
||||
result = [select_columns_serialize(_) for _ in row]
|
||||
return result
|
||||
return [select_columns_serialize(item) for item in row]
|
||||
|
||||
|
||||
def select_as_dict(row: R, use_alias: bool = False) -> dict:
|
||||
def select_as_dict(row: R, use_alias: bool = False) -> dict[str, Any]:
|
||||
"""
|
||||
Converting SQLAlchemy select to dict, which can contain relational data,
|
||||
depends on the properties of the select object itself
|
||||
将 SQLAlchemy 查询结果转换为字典,可以包含关联数据
|
||||
|
||||
If set use_alias is True, the column name will be returned as alias,
|
||||
If alias doesn't exist in columns, we don't recommend setting it to True
|
||||
|
||||
:param row:
|
||||
:param use_alias:
|
||||
:param row: SQLAlchemy 查询结果行
|
||||
:param use_alias: 是否使用别名作为列名
|
||||
:return:
|
||||
"""
|
||||
if not use_alias:
|
||||
@@ -70,7 +65,7 @@ def select_as_dict(row: R, use_alias: bool = False) -> dict:
|
||||
|
||||
class MsgSpecJSONResponse(JSONResponse):
|
||||
"""
|
||||
JSON response using the high-performance msgspec library to serialize data to JSON.
|
||||
使用高性能的 msgspec 库将数据序列化为 JSON 的响应类
|
||||
"""
|
||||
|
||||
def render(self, content: Any) -> bytes:
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
import os
|
||||
import platform
|
||||
import socket
|
||||
@@ -5,7 +7,6 @@ import sys
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import timezone as tz
|
||||
from typing import List
|
||||
|
||||
import psutil
|
||||
|
||||
@@ -14,8 +15,13 @@ from backend.utils.timezone import timezone
|
||||
|
||||
class ServerInfo:
|
||||
@staticmethod
|
||||
def format_bytes(size) -> str:
|
||||
"""格式化字节"""
|
||||
def format_bytes(size: int | float) -> str:
|
||||
"""
|
||||
格式化字节大小
|
||||
|
||||
:param size: 字节大小
|
||||
:return:
|
||||
"""
|
||||
factor = 1024
|
||||
for unit in ['', 'K', 'M', 'G', 'T', 'P', 'E', 'Z']:
|
||||
if abs(size) < factor:
|
||||
@@ -25,64 +31,64 @@ class ServerInfo:
|
||||
|
||||
@staticmethod
|
||||
def fmt_seconds(seconds: int) -> str:
|
||||
"""
|
||||
格式化秒数为可读的时间字符串
|
||||
|
||||
:param seconds: 秒数
|
||||
:return:
|
||||
"""
|
||||
days, rem = divmod(int(seconds), 86400)
|
||||
hours, rem = divmod(rem, 3600)
|
||||
minutes, seconds = divmod(rem, 60)
|
||||
|
||||
parts = []
|
||||
if days:
|
||||
parts.append('{} 天'.format(days))
|
||||
parts.append(f'{days} 天')
|
||||
if hours:
|
||||
parts.append('{} 小时'.format(hours))
|
||||
parts.append(f'{hours} 小时')
|
||||
if minutes:
|
||||
parts.append('{} 分钟'.format(minutes))
|
||||
parts.append(f'{minutes} 分钟')
|
||||
if seconds:
|
||||
parts.append('{} 秒'.format(seconds))
|
||||
if len(parts) == 0:
|
||||
return '0 秒'
|
||||
else:
|
||||
return ' '.join(parts)
|
||||
parts.append(f'{seconds} 秒')
|
||||
|
||||
return ' '.join(parts) if parts else '0 秒'
|
||||
|
||||
@staticmethod
|
||||
def fmt_timedelta(td: timedelta) -> str:
|
||||
"""格式化时间差"""
|
||||
"""
|
||||
格式化时间差
|
||||
|
||||
:param td: 时间差对象
|
||||
:return:
|
||||
"""
|
||||
total_seconds = round(td.total_seconds())
|
||||
return ServerInfo.fmt_seconds(total_seconds)
|
||||
|
||||
@staticmethod
|
||||
def get_cpu_info() -> dict:
|
||||
def get_cpu_info() -> dict[str, float | int]:
|
||||
"""获取 CPU 信息"""
|
||||
cpu_info = {'usage': round(psutil.cpu_percent(percpu=False), 2)} # %
|
||||
|
||||
# 检查是否是 Apple M系列芯片
|
||||
if platform.system() == 'Darwin' and 'arm' in platform.machine().lower():
|
||||
cpu_info['max_freq'] = 0
|
||||
cpu_info['min_freq'] = 0
|
||||
cpu_info['current_freq'] = 0
|
||||
else:
|
||||
try:
|
||||
# CPU 频率信息,最大、最小和当前频率
|
||||
cpu_freq = psutil.cpu_freq()
|
||||
cpu_info['max_freq'] = round(cpu_freq.max, 2) # MHz
|
||||
cpu_info['min_freq'] = round(cpu_freq.min, 2) # MHz
|
||||
cpu_info['current_freq'] = round(cpu_freq.current, 2) # MHz
|
||||
except FileNotFoundError:
|
||||
# 处理无法获取频率的情况
|
||||
cpu_info['max_freq'] = 0
|
||||
cpu_info['min_freq'] = 0
|
||||
cpu_info['current_freq'] = 0
|
||||
except AttributeError:
|
||||
# 处理属性不存在的情况(更安全的做法)
|
||||
cpu_info['max_freq'] = 0
|
||||
cpu_info['min_freq'] = 0
|
||||
cpu_info['current_freq'] = 0
|
||||
try:
|
||||
# CPU 频率信息,最大、最小和当前频率
|
||||
cpu_freq = psutil.cpu_freq()
|
||||
cpu_info.update({
|
||||
'max_freq': round(cpu_freq.max, 2), # MHz
|
||||
'min_freq': round(cpu_freq.min, 2), # MHz
|
||||
'current_freq': round(cpu_freq.current, 2), # MHz
|
||||
})
|
||||
except Exception:
|
||||
cpu_info.update({'max_freq': 0, 'min_freq': 0, 'current_freq': 0})
|
||||
|
||||
# CPU 逻辑核心数,物理核心数
|
||||
cpu_info['logical_num'] = psutil.cpu_count(logical=True)
|
||||
cpu_info['physical_num'] = psutil.cpu_count(logical=False)
|
||||
cpu_info.update({
|
||||
'logical_num': psutil.cpu_count(logical=True),
|
||||
'physical_num': psutil.cpu_count(logical=False),
|
||||
})
|
||||
return cpu_info
|
||||
|
||||
@staticmethod
|
||||
def get_mem_info() -> dict:
|
||||
def get_mem_info() -> dict[str, float]:
|
||||
"""获取内存信息"""
|
||||
mem = psutil.virtual_memory()
|
||||
return {
|
||||
@@ -93,7 +99,7 @@ class ServerInfo:
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_sys_info() -> dict:
|
||||
def get_sys_info() -> dict[str, str]:
|
||||
"""获取服务器信息"""
|
||||
try:
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sk:
|
||||
@@ -101,6 +107,7 @@ class ServerInfo:
|
||||
ip = sk.getsockname()[0]
|
||||
except socket.gaierror:
|
||||
ip = '127.0.0.1'
|
||||
|
||||
return {
|
||||
'name': socket.gethostname(),
|
||||
'ip': ip,
|
||||
@@ -109,7 +116,7 @@ class ServerInfo:
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def get_disk_info() -> List[dict]:
|
||||
def get_disk_info() -> list[dict[str, str]]:
|
||||
"""获取磁盘信息"""
|
||||
disk_info = []
|
||||
for disk in psutil.disk_partitions():
|
||||
@@ -126,11 +133,12 @@ class ServerInfo:
|
||||
return disk_info
|
||||
|
||||
@staticmethod
|
||||
def get_service_info():
|
||||
def get_service_info() -> dict[str, str | datetime]:
|
||||
"""获取服务信息"""
|
||||
process = psutil.Process(os.getpid())
|
||||
mem_info = process.memory_info()
|
||||
start_time = timezone.f_datetime(datetime.utcfromtimestamp(process.create_time()).replace(tzinfo=tz.utc))
|
||||
|
||||
return {
|
||||
'name': 'Python3',
|
||||
'version': platform.python_version(),
|
||||
@@ -140,7 +148,7 @@ class ServerInfo:
|
||||
'mem_rss': ServerInfo.format_bytes(mem_info.rss), # 常驻内存, 即当前进程实际使用的物理内存
|
||||
'mem_free': ServerInfo.format_bytes(mem_info.vms - mem_info.rss), # 空闲内存
|
||||
'startup': start_time,
|
||||
'elapsed': f'{ServerInfo.fmt_timedelta(timezone.now() - start_time)}',
|
||||
'elapsed': ServerInfo.fmt_timedelta(timezone.now() - start_time),
|
||||
}
|
||||
|
||||
|
||||
|
||||
+18
-16
@@ -9,32 +9,34 @@ from backend.core.conf import settings
|
||||
|
||||
|
||||
class TimeZone:
|
||||
def __init__(self, tz: str = settings.DATETIME_TIMEZONE):
|
||||
def __init__(self, tz: str = settings.DATETIME_TIMEZONE) -> None:
|
||||
"""
|
||||
初始化时区转换器
|
||||
|
||||
:param tz: 时区名称,默认为 settings.DATETIME_TIMEZONE
|
||||
:return:
|
||||
"""
|
||||
self.tz_info = zoneinfo.ZoneInfo(tz)
|
||||
|
||||
def now(self) -> datetime:
|
||||
"""
|
||||
获取时区时间
|
||||
|
||||
:return:
|
||||
"""
|
||||
"""获取当前时区时间"""
|
||||
return datetime.now(self.tz_info)
|
||||
|
||||
def f_datetime(self, dt: datetime) -> datetime:
|
||||
"""
|
||||
datetime 时间转时区时间
|
||||
将 datetime 对象转换为当前时区时间
|
||||
|
||||
:param dt:
|
||||
:param dt: 需要转换的 datetime 对象
|
||||
:return:
|
||||
"""
|
||||
return dt.astimezone(self.tz_info)
|
||||
|
||||
def f_str(self, date_str: str, format_str: str = settings.DATETIME_FORMAT) -> datetime:
|
||||
"""
|
||||
时间字符串转时区时间
|
||||
将时间字符串转换为当前时区的 datetime 对象
|
||||
|
||||
:param date_str:
|
||||
:param format_str:
|
||||
:param date_str: 时间字符串
|
||||
:param format_str: 时间格式字符串,默认为 settings.DATETIME_FORMAT
|
||||
:return:
|
||||
"""
|
||||
return datetime.strptime(date_str, format_str).replace(tzinfo=self.tz_info)
|
||||
@@ -42,10 +44,10 @@ class TimeZone:
|
||||
@staticmethod
|
||||
def t_str(dt: datetime, format_str: str = settings.DATETIME_FORMAT) -> str:
|
||||
"""
|
||||
时间转时间字符串
|
||||
将 datetime 对象转换为指定格式的时间字符串
|
||||
|
||||
:param dt:
|
||||
:param format_str:
|
||||
:param dt: datetime 对象
|
||||
:param format_str: 时间格式字符串,默认为 settings.DATETIME_FORMAT
|
||||
:return:
|
||||
"""
|
||||
return dt.strftime(format_str)
|
||||
@@ -53,9 +55,9 @@ class TimeZone:
|
||||
@staticmethod
|
||||
def f_utc(dt: datetime) -> datetime:
|
||||
"""
|
||||
时区时间转 UTC(GMT)时区
|
||||
将 datetime 对象转换为 UTC (GMT) 时区时间
|
||||
|
||||
:param dt:
|
||||
:param dt: 需要转换的 datetime 对象
|
||||
:return:
|
||||
"""
|
||||
return dt.astimezone(datetime_timezone.utc)
|
||||
|
||||
@@ -6,4 +6,10 @@ from backend.core.conf import settings
|
||||
|
||||
|
||||
def get_request_trace_id(request: Request) -> str:
|
||||
"""
|
||||
从请求头中获取追踪 ID
|
||||
|
||||
:param request: FastAPI 请求对象
|
||||
:return:
|
||||
"""
|
||||
return request.headers.get(settings.TRACE_ID_REQUEST_HEADER_KEY) or settings.LOG_CID_DEFAULT_VALUE
|
||||
|
||||
@@ -6,9 +6,9 @@ from backend.core.conf import settings
|
||||
|
||||
def sql_type_to_sqlalchemy(typing: str) -> str:
|
||||
"""
|
||||
Converts a sql type to a SQLAlchemy type.
|
||||
将 SQL 类型转换为 SQLAlchemy 类型
|
||||
|
||||
:param typing:
|
||||
:param typing: SQL 类型字符串
|
||||
:return:
|
||||
"""
|
||||
if settings.DATABASE_TYPE == 'mysql':
|
||||
@@ -22,17 +22,16 @@ def sql_type_to_sqlalchemy(typing: str) -> str:
|
||||
|
||||
def sql_type_to_pydantic(typing: str) -> str:
|
||||
"""
|
||||
Converts a sql type to a pydantic type.
|
||||
将 SQL 类型转换为 Pydantic 类型
|
||||
|
||||
:param typing:
|
||||
:param typing: SQL 类型字符串
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
if settings.DATABASE_TYPE == 'mysql':
|
||||
return GenModelMySQLColumnType[typing].value
|
||||
else:
|
||||
if typing == 'CHARACTER VARYING': # postgresql 中 DDL VARCHAR 的别名
|
||||
return 'str'
|
||||
return GenModelPostgreSQLColumnType[typing].value
|
||||
if typing == 'CHARACTER VARYING': # postgresql 中 DDL VARCHAR 的别名
|
||||
return 'str'
|
||||
return GenModelPostgreSQLColumnType[typing].value
|
||||
except KeyError:
|
||||
return 'str'
|
||||
|
||||
Reference in New Issue
Block a user