| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224 |
- """
- Repository factory for dynamically creating repository instances based on configuration.
-
- This module provides a Django-like settings system for repository implementations,
- allowing users to configure different repository backends through string paths.
- """
-
- import importlib
- import inspect
- import logging
- from typing import Protocol, Union
-
- from sqlalchemy.engine import Engine
- from sqlalchemy.orm import sessionmaker
-
- from configs import dify_config
- from core.workflow.repositories.workflow_execution_repository import WorkflowExecutionRepository
- from core.workflow.repositories.workflow_node_execution_repository import WorkflowNodeExecutionRepository
- from models import Account, EndUser
- from models.enums import WorkflowRunTriggeredFrom
- from models.workflow import WorkflowNodeExecutionTriggeredFrom
-
- logger = logging.getLogger(__name__)
-
-
- class RepositoryImportError(Exception):
- """Raised when a repository implementation cannot be imported or instantiated."""
-
- pass
-
-
- class DifyCoreRepositoryFactory:
- """
- Factory for creating repository instances based on configuration.
-
- This factory supports Django-like settings where repository implementations
- are specified as module paths (e.g., 'module.submodule.ClassName').
- """
-
- @staticmethod
- def _import_class(class_path: str) -> type:
- """
- Import a class from a module path string.
-
- Args:
- class_path: Full module path to the class (e.g., 'module.submodule.ClassName')
-
- Returns:
- The imported class
-
- Raises:
- RepositoryImportError: If the class cannot be imported
- """
- try:
- module_path, class_name = class_path.rsplit(".", 1)
- module = importlib.import_module(module_path)
- repo_class = getattr(module, class_name)
- assert isinstance(repo_class, type)
- return repo_class
- except (ValueError, ImportError, AttributeError) as e:
- raise RepositoryImportError(f"Cannot import repository class '{class_path}': {e}") from e
-
- @staticmethod
- def _validate_repository_interface(repository_class: type, expected_interface: type[Protocol]) -> None: # type: ignore
- """
- Validate that a class implements the expected repository interface.
-
- Args:
- repository_class: The class to validate
- expected_interface: The expected interface/protocol
-
- Raises:
- RepositoryImportError: If the class doesn't implement the interface
- """
- # Check if the class has all required methods from the protocol
- required_methods = [
- method
- for method in dir(expected_interface)
- if not method.startswith("_") and callable(getattr(expected_interface, method, None))
- ]
-
- missing_methods = []
- for method_name in required_methods:
- if not hasattr(repository_class, method_name):
- missing_methods.append(method_name)
-
- if missing_methods:
- raise RepositoryImportError(
- f"Repository class '{repository_class.__name__}' does not implement required methods "
- f"{missing_methods} from interface '{expected_interface.__name__}'"
- )
-
- @staticmethod
- def _validate_constructor_signature(repository_class: type, required_params: list[str]) -> None:
- """
- Validate that a repository class constructor accepts required parameters.
-
- Args:
- repository_class: The class to validate
- required_params: List of required parameter names
-
- Raises:
- RepositoryImportError: If the constructor doesn't accept required parameters
- """
-
- try:
- # MyPy may flag the line below with the following error:
- #
- # > Accessing "__init__" on an instance is unsound, since
- # > instance.__init__ could be from an incompatible subclass.
- #
- # Despite this, we need to ensure that the constructor of `repository_class`
- # has a compatible signature.
- signature = inspect.signature(repository_class.__init__) # type: ignore[misc]
- param_names = list(signature.parameters.keys())
-
- # Remove 'self' parameter
- if "self" in param_names:
- param_names.remove("self")
-
- missing_params = [param for param in required_params if param not in param_names]
- if missing_params:
- raise RepositoryImportError(
- f"Repository class '{repository_class.__name__}' constructor does not accept required parameters: "
- f"{missing_params}. Expected parameters: {required_params}"
- )
- except Exception as e:
- raise RepositoryImportError(
- f"Failed to validate constructor signature for '{repository_class.__name__}': {e}"
- ) from e
-
- @classmethod
- def create_workflow_execution_repository(
- cls,
- session_factory: Union[sessionmaker, Engine],
- user: Union[Account, EndUser],
- app_id: str,
- triggered_from: WorkflowRunTriggeredFrom,
- ) -> WorkflowExecutionRepository:
- """
- Create a WorkflowExecutionRepository instance based on configuration.
-
- Args:
- session_factory: SQLAlchemy sessionmaker or engine
- user: Account or EndUser object
- app_id: Application ID
- triggered_from: Source of the execution trigger
-
- Returns:
- Configured WorkflowExecutionRepository instance
-
- Raises:
- RepositoryImportError: If the configured repository cannot be created
- """
- class_path = dify_config.CORE_WORKFLOW_EXECUTION_REPOSITORY
- logger.debug("Creating WorkflowExecutionRepository from: %s", class_path)
-
- try:
- repository_class = cls._import_class(class_path)
- cls._validate_repository_interface(repository_class, WorkflowExecutionRepository)
- cls._validate_constructor_signature(
- repository_class, ["session_factory", "user", "app_id", "triggered_from"]
- )
-
- return repository_class( # type: ignore[no-any-return]
- session_factory=session_factory,
- user=user,
- app_id=app_id,
- triggered_from=triggered_from,
- )
- except RepositoryImportError:
- # Re-raise our custom errors as-is
- raise
- except Exception as e:
- logger.exception("Failed to create WorkflowExecutionRepository")
- raise RepositoryImportError(f"Failed to create WorkflowExecutionRepository from '{class_path}': {e}") from e
-
- @classmethod
- def create_workflow_node_execution_repository(
- cls,
- session_factory: Union[sessionmaker, Engine],
- user: Union[Account, EndUser],
- app_id: str,
- triggered_from: WorkflowNodeExecutionTriggeredFrom,
- ) -> WorkflowNodeExecutionRepository:
- """
- Create a WorkflowNodeExecutionRepository instance based on configuration.
-
- Args:
- session_factory: SQLAlchemy sessionmaker or engine
- user: Account or EndUser object
- app_id: Application ID
- triggered_from: Source of the execution trigger
-
- Returns:
- Configured WorkflowNodeExecutionRepository instance
-
- Raises:
- RepositoryImportError: If the configured repository cannot be created
- """
- class_path = dify_config.CORE_WORKFLOW_NODE_EXECUTION_REPOSITORY
- logger.debug("Creating WorkflowNodeExecutionRepository from: %s", class_path)
-
- try:
- repository_class = cls._import_class(class_path)
- cls._validate_repository_interface(repository_class, WorkflowNodeExecutionRepository)
- cls._validate_constructor_signature(
- repository_class, ["session_factory", "user", "app_id", "triggered_from"]
- )
-
- return repository_class( # type: ignore[no-any-return]
- session_factory=session_factory,
- user=user,
- app_id=app_id,
- triggered_from=triggered_from,
- )
- except RepositoryImportError:
- # Re-raise our custom errors as-is
- raise
- except Exception as e:
- logger.exception("Failed to create WorkflowNodeExecutionRepository")
- raise RepositoryImportError(
- f"Failed to create WorkflowNodeExecutionRepository from '{class_path}': {e}"
- ) from e
|