|
import enum |
|
import importlib.util |
|
import json |
|
import logging |
|
import os |
|
from pathlib import Path |
|
from typing import Any, Optional |
|
|
|
from pydantic import BaseModel |
|
|
|
from core.helper.position_helper import sort_to_dict_by_position_map |
|
|
|
|
|
class ExtensionModule(enum.Enum): |
|
MODERATION = "moderation" |
|
EXTERNAL_DATA_TOOL = "external_data_tool" |
|
|
|
|
|
class ModuleExtension(BaseModel): |
|
extension_class: Any = None |
|
name: str |
|
label: Optional[dict] = None |
|
form_schema: Optional[list] = None |
|
builtin: bool = True |
|
position: Optional[int] = None |
|
|
|
|
|
class Extensible: |
|
module: ExtensionModule |
|
|
|
name: str |
|
tenant_id: str |
|
config: Optional[dict] = None |
|
|
|
def __init__(self, tenant_id: str, config: Optional[dict] = None) -> None: |
|
self.tenant_id = tenant_id |
|
self.config = config |
|
|
|
@classmethod |
|
def scan_extensions(cls): |
|
extensions: list[ModuleExtension] = [] |
|
position_map = {} |
|
|
|
|
|
current_path = os.path.abspath(cls.__module__.replace(".", os.path.sep) + ".py") |
|
current_dir_path = os.path.dirname(current_path) |
|
|
|
|
|
for subdir_name in os.listdir(current_dir_path): |
|
if subdir_name.startswith("__"): |
|
continue |
|
|
|
subdir_path = os.path.join(current_dir_path, subdir_name) |
|
extension_name = subdir_name |
|
if os.path.isdir(subdir_path): |
|
file_names = os.listdir(subdir_path) |
|
|
|
|
|
|
|
builtin = False |
|
position = None |
|
if "__builtin__" in file_names: |
|
builtin = True |
|
|
|
builtin_file_path = os.path.join(subdir_path, "__builtin__") |
|
if os.path.exists(builtin_file_path): |
|
position = int(Path(builtin_file_path).read_text(encoding="utf-8").strip()) |
|
position_map[extension_name] = position |
|
|
|
if (extension_name + ".py") not in file_names: |
|
logging.warning(f"Missing {extension_name}.py file in {subdir_path}, Skip.") |
|
continue |
|
|
|
|
|
py_path = os.path.join(subdir_path, extension_name + ".py") |
|
spec = importlib.util.spec_from_file_location(extension_name, py_path) |
|
if not spec or not spec.loader: |
|
raise Exception(f"Failed to load module {extension_name} from {py_path}") |
|
mod = importlib.util.module_from_spec(spec) |
|
spec.loader.exec_module(mod) |
|
|
|
extension_class = None |
|
for name, obj in vars(mod).items(): |
|
if isinstance(obj, type) and issubclass(obj, cls) and obj != cls: |
|
extension_class = obj |
|
break |
|
|
|
if not extension_class: |
|
logging.warning(f"Missing subclass of {cls.__name__} in {py_path}, Skip.") |
|
continue |
|
|
|
json_data = {} |
|
if not builtin: |
|
if "schema.json" not in file_names: |
|
logging.warning(f"Missing schema.json file in {subdir_path}, Skip.") |
|
continue |
|
|
|
json_path = os.path.join(subdir_path, "schema.json") |
|
json_data = {} |
|
if os.path.exists(json_path): |
|
with open(json_path, encoding="utf-8") as f: |
|
json_data = json.load(f) |
|
|
|
extensions.append( |
|
ModuleExtension( |
|
extension_class=extension_class, |
|
name=extension_name, |
|
label=json_data.get("label"), |
|
form_schema=json_data.get("form_schema"), |
|
builtin=builtin, |
|
position=position, |
|
) |
|
) |
|
|
|
sorted_extensions = sort_to_dict_by_position_map( |
|
position_map=position_map, data=extensions, name_func=lambda x: x.name |
|
) |
|
|
|
return sorted_extensions |
|
|