import abc import importlib.metadata import inspect import importlib_metadata from mlflow.deployments.base import BaseDeploymentClient from mlflow.deployments.utils import parse_target_uri from mlflow.exceptions import MlflowException from mlflow.protos.databricks_pb2 import INTERNAL_ERROR, RESOURCE_DOES_NOT_EXIST from mlflow.utils.annotations import developer_stable from mlflow.utils.plugins import get_entry_points # TODO: refactor to have a common base class for all the plugin implementation in MLflow # mlflow/tracking/context/registry.py # mlflow/tracking/registry # mlflow/store/artifact/artifact_repository_registry.py @developer_stable class PluginManager(abc.ABC): """ Abstract class defining a entrypoint based plugin registration. This class allows the registration of a function or class to provide an implementation for a given key/name. Implementations declared though the entrypoints can be automatically registered through the `register_entrypoints` method. """ def __init__(self, group_name): self._registry = {} self.group_name = group_name self._has_registered = None @abc.abstractmethod def __getitem__(self, item): # Letting the child class create this function so that the child # can raise custom exceptions if it needs to pass @property def registry(self): """ Registry stores the registered plugin as a key value pair where key is the name of the plugin and value is the plugin object """ return self._registry @property def has_registered(self): """ Returns bool representing whether the "register_entrypoints" has run or not. This doesn't return True if `register` method is called outside of `register_entrypoints` to register plugins """ return self._has_registered def register(self, target_name, plugin_module): """Register a deployment client given its target name and module Args: target_name: The name of the deployment target. This name will be used by `get_deploy_client()` to retrieve a deployment client from the plugin store. plugin_module: The module that implements the deployment plugin interface. """ self.registry[target_name] = importlib.metadata.EntryPoint( target_name, plugin_module, self.group_name ) def register_entrypoints(self): """ Runs through all the packages that has the `group_name` defined as the entrypoint and register that into the registry """ for entrypoint in get_entry_points(self.group_name): self.registry[entrypoint.name] = entrypoint self._has_registered = True @developer_stable class DeploymentPlugins(PluginManager): def __init__(self): super().__init__("mlflow.deployments") self.register_entrypoints() def __getitem__(self, item): """Override __getitem__ so that we can directly look up plugins via dict-like syntax""" try: target_name = parse_target_uri(item) plugin_like = self.registry[target_name] except KeyError: msg = ( f'No plugin found for managing model deployments to "{item}". ' f'In order to deploy models to "{item}", find and install an appropriate ' "plugin from " "https://mlflow.org/docs/latest/plugins.html#community-plugins using " "your package manager (pip, conda etc)." ) raise MlflowException(msg, error_code=RESOURCE_DOES_NOT_EXIST) if isinstance(plugin_like, (importlib_metadata.EntryPoint, importlib.metadata.EntryPoint)): try: plugin_obj = plugin_like.load() except (AttributeError, ImportError) as exc: raise RuntimeError(f'Failed to load the plugin "{item}": {exc}') self.registry[item] = plugin_obj else: plugin_obj = plugin_like # Testing whether the plugin is valid or not expected = {"target_help", "run_local"} deployment_classes = [] for name, obj in inspect.getmembers(plugin_obj): if name in expected: expected.remove(name) elif ( inspect.isclass(obj) and issubclass(obj, BaseDeploymentClient) and not obj == BaseDeploymentClient ): deployment_classes.append(name) if len(expected) > 0: raise MlflowException( f"Plugin registered for the target {item} does not have all " "the required interfaces. Raise an issue with the " "plugin developers.\n" f"Missing interfaces: {expected}", error_code=INTERNAL_ERROR, ) if len(deployment_classes) > 1: raise MlflowException( f"Plugin registered for the target {item} has more than one " "child class of BaseDeploymentClient. Raise an issue with" " the plugin developers. " f"Classes found are {deployment_classes}" ) elif len(deployment_classes) == 0: raise MlflowException( f"Plugin registered for the target {item} has no child class" " of BaseDeploymentClient. Raise an issue with the " "plugin developers" ) return plugin_obj