775 lines
35 KiB
Python
775 lines
35 KiB
Python
from tools import *
|
||
from Utils.Config import *
|
||
from Utils.Lib import *
|
||
import importlib
|
||
import importlib.util
|
||
import os
|
||
import re
|
||
import uuid
|
||
import json
|
||
import traceback
|
||
import flask
|
||
from types import ModuleType
|
||
from typing import Dict, Callable, List, Any, Tuple, Optional, Set, Union
|
||
from datetime import datetime, timedelta
|
||
|
||
# 补充缺失的时区定义(确保代码可运行)
|
||
Timezone_UTFA8 = datetime.now().astimezone().tzinfo
|
||
|
||
ApiMetadata = Dict[str, Any]
|
||
RouteHandler = Callable[..., Any]
|
||
PermissionChecker = Callable[[int, List[Any]], bool]
|
||
ErrorHandler = Callable[[Exception], Any]
|
||
|
||
|
||
class StringLoader:
|
||
def __init__(self, code_str: str, spec):
|
||
self.code_str = code_str
|
||
self.spec = spec
|
||
def create_module(self, spec):
|
||
return None
|
||
def exec_module(self, module: ModuleType) -> None:
|
||
code = compile(
|
||
self.code_str,
|
||
filename=self.spec.origin or module.__name__,
|
||
mode='exec'
|
||
)
|
||
exec(code, module.__dict__)
|
||
|
||
|
||
class FlaskAppWrapper:
|
||
def __init__(self, App: flask.Flask, Limiter: flask_limiter.Limiter):
|
||
self.App = App
|
||
self.Limiter = Limiter
|
||
self.Blueprints: Dict[str, Dict[str, flask.Blueprint]] = {}
|
||
self.ErrorHandlers: Dict[str, Dict[int, Dict[str, ErrorHandler]]] = {}
|
||
|
||
def RegisterBlueprint(self, name: str, blueprint: Any, AllowIgnore: str = "") -> None:
|
||
self.Blueprints[name] = {
|
||
"Blueprint": blueprint,
|
||
"AllowIgnore": AllowIgnore,
|
||
"Routes": {}
|
||
}
|
||
|
||
def GetBlueprint(self, name: str) -> Tuple[flask.Blueprint, str]:
|
||
if name not in self.Blueprints:
|
||
DefaultBp = flask.Blueprint(name, __name__, url_prefix=f"/{name}")
|
||
self.App.register_blueprint(DefaultBp)
|
||
self.RegisterBlueprint(name, DefaultBp)
|
||
print(f"[{datetime.now(Timezone_UTFA8)}] Auto create blueprint: {name} (url_prefix: /{name})")
|
||
|
||
bp_info = self.Blueprints[name]
|
||
return bp_info.get("Blueprint"), bp_info.get("AllowIgnore", "")
|
||
|
||
def RegisterErrorHandler(self, BlueprintName: str, ErrorCode: int,
|
||
directory: str, handler: ErrorHandler) -> None:
|
||
if BlueprintName not in self.ErrorHandlers:
|
||
self.ErrorHandlers[BlueprintName] = {}
|
||
if ErrorCode not in self.ErrorHandlers[BlueprintName]:
|
||
self.ErrorHandlers[BlueprintName][ErrorCode] = {}
|
||
|
||
if not directory.startswith("/"): directory = f"/{directory}"
|
||
if not directory.endswith("/"): directory = f"{directory}/"
|
||
|
||
self.ErrorHandlers[BlueprintName][ErrorCode][directory] = handler
|
||
|
||
def GetBestMatchErrorHandler(self, BlueprintName: str, ErrorCode: int,
|
||
route: str) -> Optional[ErrorHandler]:
|
||
if BlueprintName not in self.ErrorHandlers or ErrorCode not in self.ErrorHandlers[BlueprintName]:
|
||
return None
|
||
|
||
handlers = self.ErrorHandlers[BlueprintName][ErrorCode]
|
||
normalized_route = route if route.endswith("/") else f"{route}/"
|
||
matched_dirs = [dir for dir in handlers if normalized_route.startswith(dir)]
|
||
|
||
if not matched_dirs:
|
||
return None
|
||
matched_dirs.sort(key=lambda x: len(x), reverse=True)
|
||
return handlers[matched_dirs[0]]
|
||
|
||
def RemoveRoute(self, BlueprintName: str, endpoint: str) -> bool:
|
||
if BlueprintName not in self.Blueprints:
|
||
return False
|
||
|
||
bp_info = self.Blueprints[BlueprintName]
|
||
if endpoint not in bp_info.get("Routes", {}):
|
||
return False
|
||
|
||
blueprint = bp_info["Blueprint"]
|
||
if endpoint in blueprint.view_functions:
|
||
del blueprint.view_functions[endpoint]
|
||
|
||
del bp_info["Routes"][endpoint]
|
||
return True
|
||
|
||
|
||
class ParameterValidator:
|
||
# 新增:时间戳字符串的默认格式
|
||
DEFAULT_TIMESTAMP_STR_FORMAT = "%Y-%m-%d %H:%M:%S"
|
||
# 新增:支持的时间戳字符串格式正则
|
||
TIMESTAMP_STR_PATTERNS = {
|
||
"%Y-%m-%d %H:%M:%S": re.compile(r'^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$'),
|
||
"%Y-%m-%d": re.compile(r'^\d{4}-\d{2}-\d{2}$'),
|
||
"%Y/%m/%d %H:%M:%S": re.compile(r'^\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}$'),
|
||
"%Y/%m/%d": re.compile(r'^\d{4}/\d{2}/\d{2}$'),
|
||
}
|
||
|
||
@staticmethod
|
||
def _IsEmptyValue(value: Any) -> bool:
|
||
"""判断值是否为空(空字符串、空列表、空字典、None)"""
|
||
if value is None:
|
||
return True
|
||
if isinstance(value, str) and value.strip() == "":
|
||
return True
|
||
if isinstance(value, (list, dict)) and len(value) == 0:
|
||
return True
|
||
return False
|
||
|
||
@staticmethod
|
||
def _ConvertValue(value: str, TargetType: str, **kwargs) -> Any:
|
||
"""
|
||
转换参数值到目标类型
|
||
:param value: 原始值
|
||
:param TargetType: 目标类型
|
||
:param kwargs: 额外参数(如TimestampFormat, TimestampMin, TimestampMax)
|
||
:return: 转换后的值
|
||
"""
|
||
if value is None:
|
||
raise ValueError("值不能为空")
|
||
|
||
# 空字符串仅在目标类型为str时返回,其他类型直接抛出错误
|
||
if isinstance(value, str) and value.strip() == "":
|
||
if TargetType != 'str':
|
||
raise ValueError(f"空字符串无法转换为 {TargetType} 类型")
|
||
return ""
|
||
|
||
try:
|
||
if TargetType == 'str':
|
||
return str(value).strip()
|
||
elif TargetType == 'int':
|
||
return int(value)
|
||
elif TargetType == 'float':
|
||
return float(value)
|
||
elif TargetType == 'dict':
|
||
return json.loads(value)
|
||
elif TargetType == 'list':
|
||
return json.loads(value)
|
||
elif TargetType == 'UUID':
|
||
UuidObj = uuid.UUID(str(value).strip())
|
||
return str(UuidObj)
|
||
elif TargetType == 'MUUID':
|
||
muuid_list = [i.strip() for i in str(value).split("\n") if i.strip()]
|
||
for uid in muuid_list:
|
||
uuid.UUID(uid)
|
||
return muuid_list
|
||
elif TargetType == 'bool':
|
||
LowerVal = str(value).strip().lower()
|
||
if LowerVal in ["true", "1", "yes"]:
|
||
return True
|
||
elif LowerVal in ["false", "0", "no"]:
|
||
return False
|
||
raise ValueError(f"无法转换为布尔值: {value}")
|
||
elif TargetType == 'json':
|
||
return json.loads(value)
|
||
# 新增:timestamp类型(数字型时间戳,单位秒/毫秒)
|
||
elif TargetType == 'timestamp':
|
||
# 转换为浮点数
|
||
ts = float(value)
|
||
# 判断是否是毫秒级时间戳(数值大于1e12)
|
||
if ts > 1e12:
|
||
ts = ts / 1000.0
|
||
|
||
# 时间戳范围校验
|
||
MinTS = kwargs.get('TimestampMin')
|
||
MaxTS = kwargs.get('TimestampMax')
|
||
current_ts = datetime.now(Timezone_UTFA8).timestamp()
|
||
|
||
# 支持相对时间(如 "-1d" 表示1天前,"+1h" 表示1小时后)
|
||
def parse_relative_time(relative_str: str) -> float:
|
||
if isinstance(relative_str, (int, float)):
|
||
return float(relative_str)
|
||
if not isinstance(relative_str, str):
|
||
raise ValueError(f"无效的时间范围格式: {relative_str}")
|
||
|
||
match = re.match(r'^([+-]?)(\d+)([smhdwy])$', relative_str.strip())
|
||
if not match:
|
||
raise ValueError(f"不支持的相对时间格式: {relative_str},支持格式:+1h/-2d/30s")
|
||
|
||
sign = match.group(1)
|
||
num = int(match.group(2))
|
||
unit = match.group(3)
|
||
|
||
delta_map = {
|
||
's': timedelta(seconds=num),
|
||
'm': timedelta(minutes=num),
|
||
'h': timedelta(hours=num),
|
||
'd': timedelta(days=num),
|
||
'w': timedelta(weeks=num),
|
||
'y': timedelta(days=num*365)
|
||
}
|
||
|
||
delta = delta_map.get(unit)
|
||
if not delta:
|
||
raise ValueError(f"不支持的时间单位: {unit}")
|
||
|
||
if sign == '-':
|
||
return current_ts - delta.total_seconds()
|
||
else:
|
||
return current_ts + delta.total_seconds()
|
||
|
||
# 校验最小值
|
||
if MinTS is not None:
|
||
MinTS_val = parse_relative_time(MinTS)
|
||
if ts < MinTS_val:
|
||
raise ValueError(f"时间戳不能早于 {datetime.fromtimestamp(MinTS_val, Timezone_UTFA8).strftime('%Y-%m-%d %H:%M:%S')} (Unix: {MinTS_val})")
|
||
|
||
# 校验最大值
|
||
if MaxTS is not None:
|
||
MaxTS_val = parse_relative_time(MaxTS)
|
||
if ts > MaxTS_val:
|
||
raise ValueError(f"时间戳不能晚于 {datetime.fromtimestamp(MaxTS_val, Timezone_UTFA8).strftime('%Y-%m-%d %H:%M:%S')} (Unix: {MaxTS_val})")
|
||
|
||
return ts
|
||
# 新增:timestampstr类型(格式化时间字符串)
|
||
elif TargetType == 'timestampstr':
|
||
ValStr = str(value).strip()
|
||
# 获取自定义格式,默认使用标准格式
|
||
TSFormat = kwargs.get('TimestampFormat', ParameterValidator.DEFAULT_TIMESTAMP_STR_FORMAT)
|
||
if not TSFormat: TSFormat = ParameterValidator.DEFAULT_TIMESTAMP_STR_FORMAT
|
||
|
||
# 验证格式是否匹配
|
||
pattern = ParameterValidator.TIMESTAMP_STR_PATTERNS.get(TSFormat)
|
||
if pattern and not pattern.match(ValStr):
|
||
raise ValueError(f"时间字符串格式错误,预期格式: {TSFormat},实际值: {ValStr}")
|
||
|
||
try:
|
||
# 解析时间字符串为datetime对象
|
||
dt = datetime.strptime(ValStr, TSFormat)
|
||
# 转换为UTC时间戳(秒)
|
||
ts = dt.replace(tzinfo=Timezone_UTFA8).timestamp()
|
||
except ValueError as e:
|
||
raise ValueError(f"时间字符串解析失败(格式:{TSFormat},值:{ValStr}): {str(e)}") from e
|
||
|
||
# 复用timestamp的范围校验逻辑
|
||
MinTS = kwargs.get('TimestampMin')
|
||
MaxTS = kwargs.get('TimestampMax')
|
||
if MinTS is not None or MaxTS is not None:
|
||
# 递归调用timestamp类型的转换逻辑来校验范围
|
||
ParameterValidator._ConvertValue(ts, 'timestamp', TimestampMin=MinTS, TimestampMax=MaxTS)
|
||
|
||
return ValStr
|
||
else:
|
||
raise ValueError(f"不支持的参数类型: {TargetType}")
|
||
except json.JSONDecodeError as e:
|
||
raise ValueError(f"JSON解析失败: {str(e)}") from e
|
||
except ValueError as e:
|
||
if "badly formed hexadecimal UUID literal" in str(e) or "invalid hex code" in str(e):
|
||
raise ValueError(f"UUID格式错误: {str(e)}") from e
|
||
raise e
|
||
except (TypeError, ValueError) as e:
|
||
print(traceback.format_exc())
|
||
raise ValueError(f"类型转换失败(预期{TargetType}): {str(e)}") from e
|
||
|
||
@staticmethod
|
||
def _ValidateType(value: Any, ParamType: str) -> bool:
|
||
if ParamType == 'str':
|
||
return isinstance(value, str)
|
||
elif ParamType == 'int':
|
||
return isinstance(value, int)
|
||
elif ParamType == 'float':
|
||
return isinstance(value, float)
|
||
elif ParamType == 'dict':
|
||
return isinstance(value, dict)
|
||
elif ParamType == 'list':
|
||
return isinstance(value, list)
|
||
elif ParamType == 'UUID':
|
||
try:
|
||
uuid.UUID(value)
|
||
return True
|
||
except:
|
||
return False
|
||
elif ParamType == 'MUUID':
|
||
return isinstance(value, list) and all(isinstance(i, str) and ParameterValidator._ValidateType(i, 'UUID') for i in value)
|
||
elif ParamType == 'bool':
|
||
return isinstance(value, bool)
|
||
elif ParamType == 'json':
|
||
return isinstance(value, (dict, list, type(None)))
|
||
# 新增:timestamp类型验证
|
||
elif ParamType == 'timestamp':
|
||
return isinstance(value, (int, float))
|
||
# 新增:timestampstr类型验证
|
||
elif ParamType == 'timestampstr':
|
||
return isinstance(value, str)
|
||
return False
|
||
|
||
@staticmethod
|
||
def _ValidateLength(value: Any, MinLen: Optional[Union[int, float]],
|
||
MaxLen: Optional[Union[int, float]], length: Optional[int],
|
||
ParamType: str) -> bool:
|
||
if MinLen is None and MaxLen is None and length is None:
|
||
return True
|
||
|
||
# 对于timestamp类型,校验的是数值范围而非长度
|
||
if ParamType == 'timestamp':
|
||
CheckValue = value
|
||
# 对于timestampstr类型,校验的是字符串长度
|
||
elif ParamType == 'timestampstr':
|
||
CheckValue = len(value)
|
||
else:
|
||
CheckValue = len(value) if ParamType in ['str', 'list', 'dict', 'UUID', 'MUUID'] else value
|
||
|
||
if length is not None and CheckValue != length:
|
||
return False
|
||
if MinLen is not None and CheckValue < MinLen:
|
||
return False
|
||
if MaxLen is not None and CheckValue > MaxLen:
|
||
return False
|
||
|
||
return True
|
||
|
||
@staticmethod
|
||
def extract_and_validate(request: flask.Request, params: List[Dict[str, Any]],
|
||
source: str) -> Dict[str, Any]:
|
||
result = {}
|
||
if source == "form":
|
||
SourceObj = request.form
|
||
elif source == "args":
|
||
SourceObj = request.args
|
||
elif source == "json":
|
||
SourceObj = request.get_json(silent=True) or {}
|
||
elif source == "headers":
|
||
SourceObj = request.headers
|
||
elif source == "cookies":
|
||
SourceObj = request.cookies
|
||
else:
|
||
raise ValueError(f"不支持的参数来源: {source}")
|
||
|
||
for param in params:
|
||
ParamName = param.get('Name')
|
||
if not ParamName:
|
||
continue
|
||
|
||
# ===== 新增:解析空值相关规则 =====
|
||
AllowedEmpty = param.get('AllowedEmpty', False) # 允许空值
|
||
PassEmpty = param.get('PassEmpty', False) # 空值直接跳过
|
||
# ================================
|
||
|
||
required = param.get('Required', True)
|
||
ParamType = param.get('Type', 'str')
|
||
MinLen = param.get('MinLen')
|
||
MaxLen = param.get('MaxLen')
|
||
length = param.get('Length')
|
||
AllowList = param.get('AllowList')
|
||
default = param.get('Default')
|
||
|
||
# 新增:提取时间戳相关的自定义参数
|
||
TimestampFormat = param.get('TimestampFormat')
|
||
TimestampMin = param.get('TimestampMin')
|
||
TimestampMax = param.get('TimestampMax')
|
||
|
||
RawValue = SourceObj.get(ParamName)
|
||
# 判断原始值是否为空
|
||
IsEmpty = ParameterValidator._IsEmptyValue(RawValue)
|
||
|
||
# ===== 核心:PassEmpty 逻辑 =====
|
||
if PassEmpty and IsEmpty:
|
||
# 空值直接跳过,不加入结果字典
|
||
continue
|
||
# ================================
|
||
|
||
# 仅当原始值不存在时使用默认值,空字符串也算有值
|
||
if RawValue is None:
|
||
if required:
|
||
raise ValueError(f"参数缺失: {ParamName}")
|
||
result[ParamName] = default
|
||
continue
|
||
|
||
# ===== 核心:AllowedEmpty 逻辑 =====
|
||
if AllowedEmpty and IsEmpty:
|
||
# 允许空值,直接返回空值(不校验类型/长度)
|
||
result[ParamName] = RawValue if isinstance(RawValue, str) else ""
|
||
continue
|
||
# ==================================
|
||
|
||
try:
|
||
# 传递时间戳相关参数到转换函数
|
||
converted_value = ParameterValidator._ConvertValue(
|
||
RawValue,
|
||
ParamType,
|
||
TimestampFormat=TimestampFormat,
|
||
TimestampMin=TimestampMin,
|
||
TimestampMax=TimestampMax
|
||
)
|
||
except Exception as e:
|
||
raise TypeError(f"参数 {ParamName} 类型错误(预期{ParamType}): {str(e)}") from e
|
||
|
||
# 仅当转换后的值为空且非必填时,才使用默认值(但已完成类型验证)
|
||
if not required and converted_value in ["", [], {}, None]:
|
||
result[ParamName] = default
|
||
continue
|
||
|
||
# 所有有值的参数(包括空字符串)都必须通过类型和长度验证
|
||
if not ParameterValidator._ValidateType(converted_value, ParamType):
|
||
raise TypeError(f"参数 {ParamName} 类型错误,预期 {ParamType}")
|
||
if not ParameterValidator._ValidateLength(converted_value, MinLen, MaxLen, length, ParamType):
|
||
msg = f"参数 {ParamName} {'长度' if ParamType in ['str', 'list', 'dict', 'timestampstr'] else '数值'} 不符合要求"
|
||
raise ValueError(msg)
|
||
|
||
if AllowList and converted_value not in AllowList:
|
||
raise ValueError(f"参数 {ParamName} 仅允许值: {AllowList}")
|
||
|
||
result[ParamName] = converted_value
|
||
|
||
return result
|
||
|
||
|
||
class ApiRouter:
|
||
def __init__(self, app_wrapper: FlaskAppWrapper, limit: Any, api_dir: str = "./TempLateAPI/"):
|
||
self.AppWrapper = app_wrapper
|
||
self.Limit = limit
|
||
self.ApiDir = os.path.abspath(api_dir)
|
||
self.VerifyPermission: PermissionChecker = lambda p, a: True
|
||
self._ErrorCodePattern = re.compile(r"Error_(\d+)\.api$")
|
||
self._ModuleCache: Dict[str, Dict[str, Any]] = {}
|
||
self._RegisteredEndpoints: Set[str] = set()
|
||
|
||
def SetPermissionVerifier(self, verifier: PermissionChecker) -> None:
|
||
self.VerifyPermission = verifier
|
||
|
||
def _LoadApiModule(self, FilePath: str, ForceReload: bool = False) -> Tuple[ModuleType, ApiMetadata]:
|
||
FilePath = os.path.abspath(FilePath)
|
||
if not os.path.exists(FilePath):
|
||
raise FileNotFoundError(f"API文件不存在: {FilePath}")
|
||
if not FilePath.endswith(".api"):
|
||
raise ValueError(f"文件不是.api类型: {FilePath}")
|
||
|
||
FileMtime = os.path.getmtime(FilePath)
|
||
CacheInfo = self._ModuleCache.get(FilePath)
|
||
|
||
if CacheInfo and not ForceReload and CacheInfo["mtime"] == FileMtime:
|
||
return CacheInfo["module"], CacheInfo["meta"]
|
||
|
||
try:
|
||
with open(FilePath, "r", encoding="UTF-8") as f:
|
||
code = f.read()
|
||
except Exception as e:
|
||
raise RuntimeError(f"读取API文件失败: {str(e)}") from e
|
||
|
||
ModuleName = f"API_{uuid.uuid4().hex}"
|
||
spec = importlib.util.spec_from_loader(ModuleName, loader=None, origin=FilePath)
|
||
spec.loader = StringLoader(code, spec)
|
||
|
||
module = importlib.util.module_from_spec(spec)
|
||
module.__FilePath__ = FilePath
|
||
spec.loader.exec_module(module)
|
||
|
||
IsErrorHandler = self._ErrorCodePattern.match(os.path.basename(FilePath))
|
||
required_func = "HandleError" if IsErrorHandler else "HandleRequest"
|
||
# 检查是否有必需的函数,但不强制要求(支持仅运行的扩展文件)
|
||
if IsErrorHandler and (not hasattr(module, required_func) or not callable(getattr(module, required_func))):
|
||
raise RuntimeError(f"错误处理API文件未实现 {required_func} 函数: {FilePath}")
|
||
# 对于普通API文件,允许没有HandleRequest函数(仅运行模式)
|
||
if not IsErrorHandler and (not hasattr(module, required_func) or not callable(getattr(module, required_func))):
|
||
# 仅运行模式,不创建路由
|
||
print(f"[{datetime.now(Timezone_UTFA8)}] Module Executed (No Route) | {FilePath}")
|
||
return module, {}
|
||
|
||
meta = getattr(module, "__metadata__", {})
|
||
meta.setdefault("methods", ["POST"])
|
||
meta.setdefault("route", None)
|
||
meta.setdefault("blueprint", "default")
|
||
meta.setdefault("permission", 0)
|
||
meta.setdefault("attributes", [])
|
||
meta.setdefault("limit", None)
|
||
meta.setdefault("restricted", "/")
|
||
meta.setdefault("form", [])
|
||
meta.setdefault("args", [])
|
||
meta.setdefault("json", [])
|
||
meta.setdefault("headers", [])
|
||
meta.setdefault("cookies", [])
|
||
|
||
self._ModuleCache[FilePath] = {
|
||
"module": module,
|
||
"meta": meta,
|
||
"mtime": FileMtime
|
||
}
|
||
|
||
return module, meta
|
||
|
||
def _CreateRouteHandler(self, module: ModuleType, meta: ApiMetadata) -> RouteHandler:
|
||
FilePath = getattr(module, "__FilePath__", None)
|
||
_last_reload_mtime = os.path.getmtime(FilePath) if FilePath and os.path.exists(FilePath) else 0
|
||
|
||
def WrappedHandler(**PathKwargs: Any) -> Any:
|
||
nonlocal module, _last_reload_mtime
|
||
|
||
if FilePath and os.path.exists(FilePath):
|
||
current_mtime = os.path.getmtime(FilePath)
|
||
if current_mtime > _last_reload_mtime:
|
||
fresh_module, _ = self._LoadApiModule(FilePath, ForceReload=True)
|
||
if fresh_module:
|
||
module = fresh_module
|
||
_last_reload_mtime = current_mtime
|
||
|
||
# 1. 权限验证:失败直接403
|
||
# 处理 permission 作为函数的情况
|
||
permission = meta["permission"]
|
||
if callable(permission):
|
||
permission = permission(**PathKwargs)
|
||
|
||
# 处理 attributes 作为函数的情况
|
||
attributes = meta["attributes"]
|
||
if callable(attributes):
|
||
attributes = attributes(**PathKwargs)
|
||
|
||
if not self.VerifyPermission(permission, attributes):
|
||
return flask.jsonify({"code": 403, "msg": "权限不足"}), 403
|
||
|
||
ValidatedData = {}
|
||
try:
|
||
# 2. 参数验证:遍历所有source,验证失败直接抛出异常
|
||
for source in ["form", "args", "json", "headers", "cookies"]:
|
||
if meta.get(source):
|
||
# 关键修改:处理可调用对象返回非列表的情况
|
||
ValidateRules = meta[source]
|
||
if callable(ValidateRules):
|
||
# 把路径参数传给验证规则生成函数
|
||
ValidateRules = ValidateRules(**PathKwargs)
|
||
|
||
# 判断返回值是否为列表:不是则直接返回响应
|
||
if not isinstance(ValidateRules, list):
|
||
# 处理 (响应体, 状态码) 元组
|
||
if isinstance(ValidateRules, tuple) and len(ValidateRules) == 2:
|
||
response_body, status_code = ValidateRules
|
||
# 统一封装为JSON响应
|
||
if isinstance(response_body, dict):
|
||
return flask.jsonify(response_body), status_code
|
||
return response_body, status_code
|
||
# 处理单个响应体(默认400状态码)
|
||
else:
|
||
if isinstance(ValidateRules, dict):
|
||
return flask.jsonify(ValidateRules), 400
|
||
return ValidateRules, 400
|
||
|
||
# 验证结果存入ValidatedData,避免覆盖
|
||
ValidatedData[source] = ParameterValidator.extract_and_validate(
|
||
flask.request, ValidateRules, source
|
||
)
|
||
# 3. 路径参数合并
|
||
if PathKwargs:
|
||
ValidatedData["params"] = PathKwargs
|
||
|
||
except ValueError as e:
|
||
# 参数缺失/长度/允许列表错误 → 400
|
||
return flask.jsonify({"code": 400, "msg": str(e)}), 400
|
||
except TypeError as e:
|
||
# 类型错误 → 422
|
||
return flask.jsonify({"code": 422, "msg": str(e)}), 422
|
||
except Exception as e:
|
||
# 其他验证异常 → 400
|
||
return flask.jsonify({"code": 400, "msg": f"参数验证失败: {str(e)}"}), 400
|
||
|
||
ValidatedData = {
|
||
"form": ValidatedData["form"] if "form" in ValidatedData else {},
|
||
"args": ValidatedData["args"] if "args" in ValidatedData else {},
|
||
"json": ValidatedData["json"] if "json" in ValidatedData else {},
|
||
"headers": ValidatedData["headers"] if "headers" in ValidatedData else {},
|
||
"cookies": ValidatedData["cookies"] if "cookies" in ValidatedData else {},
|
||
"params": ValidatedData["params"] if "params" in ValidatedData else {}
|
||
}
|
||
|
||
# 4. 验证通过后,才执行HandleRequest
|
||
handler = getattr(module, "HandleRequest", None)
|
||
if not handler or not callable(handler):
|
||
return flask.jsonify({"code": 500, "msg": "API未实现HandleRequest函数"}), 500
|
||
|
||
sig = inspect.signature(handler)
|
||
params = sig.parameters
|
||
|
||
HasCleanedDataParam = False
|
||
if len(params) == 1 and "_CleanedData" in params:
|
||
HasCleanedDataParam = True
|
||
|
||
try:
|
||
if HasCleanedDataParam:
|
||
result = handler(ValidatedData)
|
||
else:
|
||
result = handler()
|
||
# 统一返回格式(确保是响应对象)
|
||
if isinstance(result, tuple) and len(result) == 2:
|
||
return result
|
||
return flask.jsonify(result) if isinstance(result, dict) else result
|
||
except Exception as e:
|
||
# 业务逻辑异常 → 400,避免500
|
||
traceback.print_exc()
|
||
return flask.jsonify({"code": 400, "msg": f"业务处理失败: {str(e)}"}), 400
|
||
|
||
return WrappedHandler
|
||
|
||
def _CreateErrorHandler(self, module: ModuleType, meta: ApiMetadata) -> ErrorHandler:
|
||
def WrappedErrorHandler(error: Exception) -> Any:
|
||
handler = getattr(module, "HandleError", None)
|
||
if not handler or not callable(handler):
|
||
raise NotImplementedError("Error API must implement HandleError function")
|
||
return handler(error)
|
||
return WrappedErrorHandler
|
||
|
||
def _GenerateFilePathRoute(self, FilePath: str, AllowIgnore: str) -> str:
|
||
RelativePath = os.path.relpath(FilePath, self.ApiDir)
|
||
BaseRoute = "/" + os.path.splitext(RelativePath)[0].replace(os.sep, "/")
|
||
|
||
if AllowIgnore:
|
||
IgnorePrefix = f"/{AllowIgnore.lstrip('/')}"
|
||
if BaseRoute.startswith(IgnorePrefix):
|
||
BaseRoute = BaseRoute[len(IgnorePrefix):]
|
||
if not BaseRoute.startswith("/"):
|
||
BaseRoute = f"/{BaseRoute}"
|
||
|
||
return BaseRoute
|
||
|
||
def _GenerateUniqueEndpoint(self, FilePath: str, BlueprintName: str) -> str:
|
||
RelativePath = os.path.relpath(FilePath, self.ApiDir)
|
||
PathPart = os.path.splitext(RelativePath)[0].replace(os.sep, "_").replace(".", "_")
|
||
endpoint = f"{PathPart}"
|
||
|
||
while endpoint in self._RegisteredEndpoints:
|
||
endpoint += f"_{uuid.uuid4().hex[:8]}"
|
||
|
||
return endpoint
|
||
|
||
def RegisterApi(self, FilePath: str, ForceReload: bool = False) -> None:
|
||
try:
|
||
module, meta = self._LoadApiModule(FilePath, ForceReload)
|
||
file_name = os.path.basename(FilePath)
|
||
ErrorMatch = self._ErrorCodePattern.match(file_name)
|
||
|
||
# 检查是否为仅运行模式(meta为空字典)
|
||
if not meta:
|
||
# 仅运行模式,不创建路由,直接返回
|
||
return
|
||
|
||
if ErrorMatch:
|
||
ErrorCode = int(ErrorMatch.group(1))
|
||
BlueprintName = meta["blueprint"]
|
||
RestrictedDir = meta["restricted"]
|
||
|
||
ErrorHandler = self._CreateErrorHandler(module, meta)
|
||
self.AppWrapper.RegisterErrorHandler(
|
||
BlueprintName=BlueprintName,
|
||
ErrorCode=ErrorCode,
|
||
directory=RestrictedDir,
|
||
handler=ErrorHandler
|
||
)
|
||
print(f"[{datetime.now(Timezone_UTFA8)}] Error Handler Registered | {BlueprintName} | {ErrorCode} | {RestrictedDir}")
|
||
else:
|
||
blueprint, AllowIgnore = self.AppWrapper.GetBlueprint(meta["blueprint"])
|
||
DefaultRoute = self._GenerateFilePathRoute(FilePath, AllowIgnore)
|
||
if meta.get("route"): route = meta["route"].replace("{DEFAULT_ROUTE}", DefaultRoute)
|
||
else: route = DefaultRoute
|
||
endpoint = self._GenerateUniqueEndpoint(FilePath, meta["blueprint"])
|
||
|
||
if endpoint in self._RegisteredEndpoints:
|
||
self.AppWrapper.RemoveRoute(meta["blueprint"], endpoint)
|
||
self._RegisteredEndpoints.remove(endpoint)
|
||
|
||
handler = self._CreateRouteHandler(module, meta)
|
||
|
||
blueprint.add_url_rule(
|
||
rule=route,
|
||
view_func=handler,
|
||
methods=meta["methods"],
|
||
endpoint=endpoint
|
||
)
|
||
|
||
self._RegisteredEndpoints.add(endpoint)
|
||
self.AppWrapper.Blueprints[meta["blueprint"]]["Routes"][endpoint] = {
|
||
"route": route,
|
||
"FilePath": FilePath,
|
||
"module": module
|
||
}
|
||
|
||
print(f"[{datetime.now(Timezone_UTFA8)}] Route Registered | {endpoint} | {route} | {meta['methods']} | {meta['blueprint']}")
|
||
|
||
except Exception as e:
|
||
route = meta.get("route", "") if 'meta' in locals() else ""
|
||
print(f"[{datetime.now(Timezone_UTFA8)}] Failed to register API | {FilePath} | {route} | Error: {str(e)}")
|
||
|
||
def RegisterAllApis(self, ForceReload: bool = False) -> None:
|
||
if not os.path.exists(self.ApiDir):
|
||
os.makedirs(self.ApiDir)
|
||
print(f"[{datetime.now(Timezone_UTFA8)}] API目录不存在,已创建: {self.ApiDir}")
|
||
return
|
||
|
||
for root, _, files in os.walk(self.ApiDir):
|
||
for file in files:
|
||
if file.endswith(".api"):
|
||
self.RegisterApi(os.path.join(root, file), ForceReload)
|
||
|
||
self._RegisterErrorDispatchers()
|
||
|
||
def HotReloadApi(self, FilePath: Optional[str] = None) -> None:
|
||
now = datetime.now(Timezone_UTFA8).timestamp()
|
||
|
||
if FilePath:
|
||
if FilePath in self._ModuleCache:
|
||
del self._ModuleCache[FilePath]
|
||
print(f"[{datetime.now(Timezone_UTFA8)}] Hot reload: {FilePath}")
|
||
else:
|
||
self._ModuleCache.clear()
|
||
print(f"[{datetime.now(Timezone_UTFA8)}] Hot reload completed | All GERP API modules cleared")
|
||
|
||
ExpiredPaths = [path for path, info in self._ModuleCache.items()
|
||
if not os.path.exists(path) or (now - info["mtime"]) > 600]
|
||
for path in ExpiredPaths:
|
||
del self._ModuleCache[path]
|
||
|
||
def _RegisterErrorDispatchers(self) -> None:
|
||
for BlueprintName, blueprint_data in self.AppWrapper.Blueprints.items():
|
||
blueprint = blueprint_data["Blueprint"]
|
||
|
||
def CreateErrorDispatcher(current_BlueprintName: str):
|
||
@blueprint.errorhandler(Exception)
|
||
def ErrorDispatcher(error: Exception) -> Any:
|
||
from werkzeug.exceptions import HTTPException
|
||
if isinstance(error, HTTPException):
|
||
ErrorCode = error.code
|
||
error_msg = error.description
|
||
else:
|
||
ErrorCode = getattr(error, 'code', 500)
|
||
error_msg = str(error)
|
||
|
||
ErrorCode = ErrorCode if isinstance(ErrorCode, int) else 500
|
||
|
||
route = flask.request.path
|
||
handler = self.AppWrapper.GetBestMatchErrorHandler(
|
||
BlueprintName=current_BlueprintName,
|
||
ErrorCode=ErrorCode,
|
||
route=route
|
||
)
|
||
|
||
if handler:
|
||
try:
|
||
return handler(error)
|
||
except Exception as e:
|
||
print(f"[{datetime.now(Timezone_UTFA8)}] Custom error handler failed | {current_BlueprintName} | {ErrorCode} | {str(e)}")
|
||
|
||
if ErrorCode == 500:
|
||
print(f"[{datetime.now(Timezone_UTFA8)}] Server Error | {traceback.format_exc()}")
|
||
|
||
return flask.jsonify({
|
||
"code": ErrorCode,
|
||
"msg": error_msg,
|
||
"timestamp": datetime.now(Timezone_UTFA8).timestamp(),
|
||
"path": flask.request.path
|
||
}), ErrorCode
|
||
|
||
return ErrorDispatcher
|
||
|
||
CreateErrorDispatcher(BlueprintName)
|
||
|
||
def Cleanup(self) -> None:
|
||
self._ModuleCache.clear()
|
||
self._RegisteredEndpoints.clear()
|
||
for BlueprintName in self.AppWrapper.Blueprints:
|
||
self.AppWrapper.Blueprints[BlueprintName]["Routes"].clear()
|
||
print(f"[{datetime.now(Timezone_UTFA8)}] Resource cleanup completed") |