Files
mlflow--mlflow/mlflow/deployments/plugin_manager.py
T
2026-07-13 13:22:34 +08:00

144 lines
5.5 KiB
Python

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