Files
SMANAGE/backup/_flask_backup/TempLateModule/FileSystemRoute.py
T

775 lines
35 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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")