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

factory.py 8.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. """
  2. Repository factory for dynamically creating repository instances based on configuration.
  3. This module provides a Django-like settings system for repository implementations,
  4. allowing users to configure different repository backends through string paths.
  5. """
  6. import importlib
  7. import inspect
  8. import logging
  9. from typing import Protocol, Union
  10. from sqlalchemy.engine import Engine
  11. from sqlalchemy.orm import sessionmaker
  12. from configs import dify_config
  13. from core.workflow.repositories.workflow_execution_repository import WorkflowExecutionRepository
  14. from core.workflow.repositories.workflow_node_execution_repository import WorkflowNodeExecutionRepository
  15. from models import Account, EndUser
  16. from models.enums import WorkflowRunTriggeredFrom
  17. from models.workflow import WorkflowNodeExecutionTriggeredFrom
  18. logger = logging.getLogger(__name__)
  19. class RepositoryImportError(Exception):
  20. """Raised when a repository implementation cannot be imported or instantiated."""
  21. pass
  22. class DifyCoreRepositoryFactory:
  23. """
  24. Factory for creating repository instances based on configuration.
  25. This factory supports Django-like settings where repository implementations
  26. are specified as module paths (e.g., 'module.submodule.ClassName').
  27. """
  28. @staticmethod
  29. def _import_class(class_path: str) -> type:
  30. """
  31. Import a class from a module path string.
  32. Args:
  33. class_path: Full module path to the class (e.g., 'module.submodule.ClassName')
  34. Returns:
  35. The imported class
  36. Raises:
  37. RepositoryImportError: If the class cannot be imported
  38. """
  39. try:
  40. module_path, class_name = class_path.rsplit(".", 1)
  41. module = importlib.import_module(module_path)
  42. repo_class = getattr(module, class_name)
  43. assert isinstance(repo_class, type)
  44. return repo_class
  45. except (ValueError, ImportError, AttributeError) as e:
  46. raise RepositoryImportError(f"Cannot import repository class '{class_path}': {e}") from e
  47. @staticmethod
  48. def _validate_repository_interface(repository_class: type, expected_interface: type[Protocol]) -> None: # type: ignore
  49. """
  50. Validate that a class implements the expected repository interface.
  51. Args:
  52. repository_class: The class to validate
  53. expected_interface: The expected interface/protocol
  54. Raises:
  55. RepositoryImportError: If the class doesn't implement the interface
  56. """
  57. # Check if the class has all required methods from the protocol
  58. required_methods = [
  59. method
  60. for method in dir(expected_interface)
  61. if not method.startswith("_") and callable(getattr(expected_interface, method, None))
  62. ]
  63. missing_methods = []
  64. for method_name in required_methods:
  65. if not hasattr(repository_class, method_name):
  66. missing_methods.append(method_name)
  67. if missing_methods:
  68. raise RepositoryImportError(
  69. f"Repository class '{repository_class.__name__}' does not implement required methods "
  70. f"{missing_methods} from interface '{expected_interface.__name__}'"
  71. )
  72. @staticmethod
  73. def _validate_constructor_signature(repository_class: type, required_params: list[str]) -> None:
  74. """
  75. Validate that a repository class constructor accepts required parameters.
  76. Args:
  77. repository_class: The class to validate
  78. required_params: List of required parameter names
  79. Raises:
  80. RepositoryImportError: If the constructor doesn't accept required parameters
  81. """
  82. try:
  83. # MyPy may flag the line below with the following error:
  84. #
  85. # > Accessing "__init__" on an instance is unsound, since
  86. # > instance.__init__ could be from an incompatible subclass.
  87. #
  88. # Despite this, we need to ensure that the constructor of `repository_class`
  89. # has a compatible signature.
  90. signature = inspect.signature(repository_class.__init__) # type: ignore[misc]
  91. param_names = list(signature.parameters.keys())
  92. # Remove 'self' parameter
  93. if "self" in param_names:
  94. param_names.remove("self")
  95. missing_params = [param for param in required_params if param not in param_names]
  96. if missing_params:
  97. raise RepositoryImportError(
  98. f"Repository class '{repository_class.__name__}' constructor does not accept required parameters: "
  99. f"{missing_params}. Expected parameters: {required_params}"
  100. )
  101. except Exception as e:
  102. raise RepositoryImportError(
  103. f"Failed to validate constructor signature for '{repository_class.__name__}': {e}"
  104. ) from e
  105. @classmethod
  106. def create_workflow_execution_repository(
  107. cls,
  108. session_factory: Union[sessionmaker, Engine],
  109. user: Union[Account, EndUser],
  110. app_id: str,
  111. triggered_from: WorkflowRunTriggeredFrom,
  112. ) -> WorkflowExecutionRepository:
  113. """
  114. Create a WorkflowExecutionRepository instance based on configuration.
  115. Args:
  116. session_factory: SQLAlchemy sessionmaker or engine
  117. user: Account or EndUser object
  118. app_id: Application ID
  119. triggered_from: Source of the execution trigger
  120. Returns:
  121. Configured WorkflowExecutionRepository instance
  122. Raises:
  123. RepositoryImportError: If the configured repository cannot be created
  124. """
  125. class_path = dify_config.CORE_WORKFLOW_EXECUTION_REPOSITORY
  126. logger.debug("Creating WorkflowExecutionRepository from: %s", class_path)
  127. try:
  128. repository_class = cls._import_class(class_path)
  129. cls._validate_repository_interface(repository_class, WorkflowExecutionRepository)
  130. cls._validate_constructor_signature(
  131. repository_class, ["session_factory", "user", "app_id", "triggered_from"]
  132. )
  133. return repository_class( # type: ignore[no-any-return]
  134. session_factory=session_factory,
  135. user=user,
  136. app_id=app_id,
  137. triggered_from=triggered_from,
  138. )
  139. except RepositoryImportError:
  140. # Re-raise our custom errors as-is
  141. raise
  142. except Exception as e:
  143. logger.exception("Failed to create WorkflowExecutionRepository")
  144. raise RepositoryImportError(f"Failed to create WorkflowExecutionRepository from '{class_path}': {e}") from e
  145. @classmethod
  146. def create_workflow_node_execution_repository(
  147. cls,
  148. session_factory: Union[sessionmaker, Engine],
  149. user: Union[Account, EndUser],
  150. app_id: str,
  151. triggered_from: WorkflowNodeExecutionTriggeredFrom,
  152. ) -> WorkflowNodeExecutionRepository:
  153. """
  154. Create a WorkflowNodeExecutionRepository instance based on configuration.
  155. Args:
  156. session_factory: SQLAlchemy sessionmaker or engine
  157. user: Account or EndUser object
  158. app_id: Application ID
  159. triggered_from: Source of the execution trigger
  160. Returns:
  161. Configured WorkflowNodeExecutionRepository instance
  162. Raises:
  163. RepositoryImportError: If the configured repository cannot be created
  164. """
  165. class_path = dify_config.CORE_WORKFLOW_NODE_EXECUTION_REPOSITORY
  166. logger.debug("Creating WorkflowNodeExecutionRepository from: %s", class_path)
  167. try:
  168. repository_class = cls._import_class(class_path)
  169. cls._validate_repository_interface(repository_class, WorkflowNodeExecutionRepository)
  170. cls._validate_constructor_signature(
  171. repository_class, ["session_factory", "user", "app_id", "triggered_from"]
  172. )
  173. return repository_class( # type: ignore[no-any-return]
  174. session_factory=session_factory,
  175. user=user,
  176. app_id=app_id,
  177. triggered_from=triggered_from,
  178. )
  179. except RepositoryImportError:
  180. # Re-raise our custom errors as-is
  181. raise
  182. except Exception as e:
  183. logger.exception("Failed to create WorkflowNodeExecutionRepository")
  184. raise RepositoryImportError(
  185. f"Failed to create WorkflowNodeExecutionRepository from '{class_path}': {e}"
  186. ) from e