extensible.py 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. import enum
  2. import importlib.util
  3. import json
  4. import logging
  5. import os
  6. from collections import OrderedDict
  7. from typing import Any, Optional
  8. from pydantic import BaseModel
  9. class ExtensionModule(enum.Enum):
  10. MODERATION = 'moderation'
  11. EXTERNAL_DATA_TOOL = 'external_data_tool'
  12. class ModuleExtension(BaseModel):
  13. extension_class: Any
  14. name: str
  15. label: Optional[dict] = None
  16. form_schema: Optional[list] = None
  17. builtin: bool = True
  18. position: Optional[int] = None
  19. class Extensible:
  20. module: ExtensionModule
  21. name: str
  22. tenant_id: str
  23. config: Optional[dict] = None
  24. def __init__(self, tenant_id: str, config: Optional[dict] = None) -> None:
  25. self.tenant_id = tenant_id
  26. self.config = config
  27. @classmethod
  28. def scan_extensions(cls):
  29. extensions = {}
  30. # get the path of the current class
  31. current_path = os.path.abspath(cls.__module__.replace(".", os.path.sep) + '.py')
  32. current_dir_path = os.path.dirname(current_path)
  33. # traverse subdirectories
  34. for subdir_name in os.listdir(current_dir_path):
  35. if subdir_name.startswith('__'):
  36. continue
  37. subdir_path = os.path.join(current_dir_path, subdir_name)
  38. extension_name = subdir_name
  39. if os.path.isdir(subdir_path):
  40. file_names = os.listdir(subdir_path)
  41. # is builtin extension, builtin extension
  42. # in the front-end page and business logic, there are special treatments.
  43. builtin = False
  44. position = None
  45. if '__builtin__' in file_names:
  46. builtin = True
  47. builtin_file_path = os.path.join(subdir_path, '__builtin__')
  48. if os.path.exists(builtin_file_path):
  49. with open(builtin_file_path, 'r') as f:
  50. position = int(f.read().strip())
  51. if (extension_name + '.py') not in file_names:
  52. logging.warning(f"Missing {extension_name}.py file in {subdir_path}, Skip.")
  53. continue
  54. # Dynamic loading {subdir_name}.py file and find the subclass of Extensible
  55. py_path = os.path.join(subdir_path, extension_name + '.py')
  56. spec = importlib.util.spec_from_file_location(extension_name, py_path)
  57. mod = importlib.util.module_from_spec(spec)
  58. spec.loader.exec_module(mod)
  59. extension_class = None
  60. for name, obj in vars(mod).items():
  61. if isinstance(obj, type) and issubclass(obj, cls) and obj != cls:
  62. extension_class = obj
  63. break
  64. if not extension_class:
  65. logging.warning(f"Missing subclass of {cls.__name__} in {py_path}, Skip.")
  66. continue
  67. json_data = {}
  68. if not builtin:
  69. if 'schema.json' not in file_names:
  70. logging.warning(f"Missing schema.json file in {subdir_path}, Skip.")
  71. continue
  72. json_path = os.path.join(subdir_path, 'schema.json')
  73. json_data = {}
  74. if os.path.exists(json_path):
  75. with open(json_path, 'r') as f:
  76. json_data = json.load(f)
  77. extensions[extension_name] = ModuleExtension(
  78. extension_class=extension_class,
  79. name=extension_name,
  80. label=json_data.get('label'),
  81. form_schema=json_data.get('form_schema'),
  82. builtin=builtin,
  83. position=position
  84. )
  85. sorted_items = sorted(extensions.items(), key=lambda x: (x[1].position is None, x[1].position))
  86. sorted_extensions = OrderedDict(sorted_items)
  87. return sorted_extensions