You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

213 lines
7.0 KiB
Python

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import importlib
import inspect
import os
from functools import lru_cache
from pathlib import Path
from typing import Any
from sqlalchemy import inspect as sa_inspect
from app.config.path_conf import BASE_DIR
from app.core.exceptions import CustomException
class ImportUtil:
"""
扫描工程中的 ORM 模型文件并做有效性校验的辅助类。
"""
@classmethod
def find_project_root(cls) -> Path:
"""
返回项目根目录(与配置中的 `BASE_DIR` 一致)。
返回:
- Path: 项目根路径。
"""
return BASE_DIR
@classmethod
def is_valid_model(cls, obj: Any, base_class: type) -> bool:
"""
判断是否为可映射的 SQLAlchemy 模型类(含表名与非空列)。
参数:
- obj (Any): 待验证对象(一般为类)。
- base_class (type): ORM 声明基类。
返回:
- bool: 是否为有效模型类。
"""
# 必须继承自base_class且不是base_class本身
if not (inspect.isclass(obj) and issubclass(obj, base_class) and obj is not base_class):
return False
# 必须有表名定义(排除抽象基类)
if not hasattr(obj, "__tablename__") or obj.__tablename__ is None:
return False
# 必须有至少一个列定义
try:
return len(sa_inspect(obj).columns) > 0
except Exception:
return False
@classmethod
@lru_cache(maxsize=256)
def find_models(cls, base_class: type) -> list[Any]:
"""
遍历工程内 `model.py` / `models.py`,收集去重后的有效模型类。
参数:
- base_class (type): SQLAlchemy 声明基类。
返回:
- list[Any]: 模型类列表。
异常:
- ImportError: 模块导入失败(非「无法从某名导入」类警告)。
- CustomException: 处理模块时发生未预期错误。
"""
models = []
# 按类对象去重
seen_models = set()
# 按表名去重(防止同表名冲突)
seen_tables = set()
# 记录已经处理过的model.py文件路径
processed_model_files = set()
project_root = cls.find_project_root()
# 排除目录扩展
exclude_dirs = {
"venv",
".env",
".git",
"__pycache__",
"migrations",
"alembic",
"tests",
"test",
"docs",
"examples",
"scripts",
".venv",
"static",
"templates",
"sql",
"env",
}
# 定义要搜索的模型目录模式
model_dir_patterns = ["model.py", "models.py"]
# 使用一个更高效的方法来查找所有model.py文件
model_files = []
for root, dirs, files in os.walk(project_root):
# 过滤排除目录
dirs[:] = [d for d in dirs if d not in exclude_dirs]
for file in files:
if file in model_dir_patterns:
file_path = Path(root) / file
# 构建相对于项目根的模块路径
relative_path = file_path.relative_to(project_root)
model_files.append((file_path, relative_path))
# 按模块路径排序,确保先导入基础模块
model_files.sort(key=lambda x: str(x[1]))
for file_path, relative_path in model_files:
# 确保文件路径没有被处理过
if str(file_path) in processed_model_files:
continue
processed_model_files.add(str(file_path))
# 构建模块名(将路径分隔符转换为点)
module_parts = (*relative_path.parts[:-1], relative_path.stem)
module_name = ".".join(module_parts)
try:
# 导入模块
module = importlib.import_module(module_name)
# 获取模块中的所有类
for _name, obj in inspect.getmembers(module, inspect.isclass):
# 验证模型有效性
if not cls.is_valid_model(obj, base_class):
continue
# 检查类对象重复
if obj in seen_models:
continue
# 检查表名重复
table_name = obj.__tablename__
if table_name in seen_tables:
continue
# 添加到已处理集合
seen_models.add(obj)
seen_tables.add(table_name)
models.append(obj)
except ImportError as e:
if "cannot import name" not in str(e):
raise ImportError(f"❗️ 警告: 无法导入模块 {module_name}: {e}")
except Exception as e:
raise CustomException(f"❌️ 处理模块 {module_name} 时出错: {e}")
# 查找apscheduler_jobs表的模型如果存在
cls._find_apscheduler_model(base_class, models, seen_models, seen_tables)
return models
@classmethod
def _find_apscheduler_model(
cls,
base_class: type,
models: list[Any],
seen_models: set[Any],
seen_tables: set[str],
) -> None:
"""
尝试从调度相关模块补充 `apscheduler_jobs` 表对应模型。
参数:
- base_class (type): ORM 声明基类。
- models (list[Any]): 已收集模型列表(就地追加)。
- seen_models (set[Any]): 已见模型对象集合。
- seen_tables (set[str]): 已见表名集合。
返回:
- None
异常:
- CustomException: 扫描过程出现未预期错误。
"""
# 尝试从apscheduler相关模块导入
try:
# 检查是否有自定义的apscheduler模型
for module_name in [
"app.core.ap_scheduler",
"app.module_task.scheduler_test",
]:
try:
module = importlib.import_module(module_name)
for _name, obj in inspect.getmembers(module, inspect.isclass):
if (
cls.is_valid_model(obj, base_class)
and hasattr(obj, "__tablename__")
and obj.__tablename__ == "apscheduler_jobs"
) and (obj not in seen_models and "apscheduler_jobs" not in seen_tables):
seen_models.add(obj)
seen_tables.add("apscheduler_jobs")
models.append(obj)
print(
f"✅️ 找到有效模型: {obj.__module__}.{obj.__name__} (表: apscheduler_jobs)"
)
except ImportError:
pass
except Exception as e:
raise CustomException(f"❗️ 查找APScheduler模型时出错: {e}")