/ mlflow / deployments / cli.py
cli.py
  1  import json
  2  import sys
  3  from inspect import signature
  4  
  5  import click
  6  
  7  from mlflow.deployments import interface
  8  from mlflow.mcp.decorator import mlflow_mcp
  9  from mlflow.utils import cli_args
 10  from mlflow.utils.proto_json_utils import NumpyEncoder, _get_jsonable_obj
 11  
 12  
 13  def _user_args_to_dict(user_list):
 14      # Similar function in mlflow.cli is throwing exception on import
 15      user_dict = {}
 16      for s in user_list:
 17          try:
 18              # Some configs may contain '=' in the value
 19              name, value = s.split("=", 1)
 20          except ValueError as exc:
 21              # not enough values to unpack
 22              raise click.BadOptionUsage(
 23                  "config",
 24                  "Config options must be a pair and should be "
 25                  "provided as ``-C key=value`` or "
 26                  "``--config key=value``",
 27              ) from exc
 28          if name in user_dict:
 29              raise click.ClickException(f"Repeated parameter: '{name}'")
 30          user_dict[name] = value
 31      return user_dict
 32  
 33  
 34  installed_targets = list(interface.plugin_store.registry)
 35  if len(installed_targets) > 0:
 36      supported_targets_msg = "Support is currently installed for deployment to: {targets}".format(
 37          targets=", ".join(installed_targets)
 38      )
 39  else:
 40      supported_targets_msg = (
 41          "NOTE: you currently do not have support installed for any deployment targets."
 42      )
 43  
 44  target_details = click.option(
 45      "--target",
 46      "-t",
 47      required=True,
 48      help=f"""
 49                                     Deployment target URI. Run
 50                                     `mlflow deployments help --target-name <target-name>` for
 51                                     more details on the supported URI format and config options
 52                                     for a given target.
 53                                     {supported_targets_msg}
 54  
 55                                     See all supported deployment targets and installation
 56                                     instructions at
 57                                     https://mlflow.org/docs/latest/plugins.html#community-plugins
 58                                     """,
 59  )
 60  deployment_name = click.option("--name", "name", required=True, help="Name of the deployment")
 61  optional_deployment_name = click.option("--name", "name", help="Name of the deployment")
 62  parse_custom_arguments = click.option(
 63      "--config",
 64      "-C",
 65      metavar="NAME=VALUE",
 66      multiple=True,
 67      help="Extra target-specific config for the model "
 68      "deployment, of the form -C name=value. See "
 69      "documentation/help for your deployment target for a "
 70      "list of supported config options.",
 71  )
 72  
 73  parse_input = click.option(
 74      "--input-path",
 75      "-I",
 76      required=True,
 77      help="Path to input prediction payload file. The file can"
 78      "be a JSON (Python Dict) or CSV (pandas DataFrame). If the file is a CSV, the user must specify"
 79      "the --content-type csv option.",
 80  )
 81  
 82  parse_output = click.option(
 83      "--output-path",
 84      "-O",
 85      help="File to output results to as a JSON file. If not provided, prints output to stdout.",
 86  )
 87  
 88  required_endpoint_param = click.option("--endpoint", required=True, help="Name of the endpoint")
 89  optional_endpoint_param = click.option("--endpoint", help="Name of the endpoint")
 90  
 91  
 92  @click.group(
 93      "deployments",
 94      help=f"""
 95      Deploy MLflow models to custom targets.
 96      Run `mlflow deployments help --target-name <target-name>` for
 97      more details on the supported URI format and config options for a given target.
 98      {supported_targets_msg}
 99  
100      See all supported deployment targets and installation instructions in
101      https://mlflow.org/docs/latest/plugins.html#community-plugins
102  
103      You can also write your own plugin for deployment to a custom target. For instructions on
104      writing and distributing a plugin, see
105      https://mlflow.org/docs/latest/plugins.html#writing-your-own-mlflow-plugins.
106  """,
107  )
108  def commands():
109      """
110      Deploy MLflow models to custom targets. Support is currently installed for
111      the following targets: {targets}. Run `mlflow deployments help --target-name <target-name>` for
112      more details on the supported URI format and config options for a given target.
113  
114      To deploy to other targets, you must first install an
115      appropriate third-party Python plugin. See the list of known community-maintained plugins
116      at https://mlflow.org/docs/latest/plugins.html#community-plugins.
117  
118      You can also write your own plugin for deployment to a custom target. For instructions on
119      writing and distributing a plugin, see
120      https://mlflow.org/docs/latest/plugins.html#writing-your-own-mlflow-plugins.
121      """
122  
123  
124  @commands.command("create")
125  @mlflow_mcp(tool_name="create_deployment")
126  @optional_endpoint_param
127  @parse_custom_arguments
128  @deployment_name
129  @target_details
130  @cli_args.MODEL_URI
131  @click.option(
132      "--flavor",
133      "-f",
134      help="Which flavor to be deployed. This will be auto inferred if it's not given",
135  )
136  def create_deployment(flavor, model_uri, target, name, config, endpoint):
137      """
138      Deploy the model at ``model_uri`` to the specified target.
139  
140      Additional plugin-specific arguments may also be passed to this command, via `-C key=value`
141      """
142      config_dict = _user_args_to_dict(config)
143      client = interface.get_deploy_client(target)
144  
145      sig = signature(client.create_deployment)
146      if "endpoint" in sig.parameters:
147          deployment = client.create_deployment(
148              name, model_uri, flavor, config=config_dict, endpoint=endpoint
149          )
150      else:
151          deployment = client.create_deployment(name, model_uri, flavor, config=config_dict)
152      click.echo("\n{} deployment {} is created".format(deployment["flavor"], deployment["name"]))
153  
154  
155  @commands.command("update")
156  @mlflow_mcp(tool_name="update_deployment")
157  @optional_endpoint_param
158  @parse_custom_arguments
159  @deployment_name
160  @target_details
161  @click.option(
162      "--model-uri",
163      "-m",
164      default=None,
165      metavar="URI",
166      help="URI to the model. A local path, a 'runs:/' URI, or a"
167      " remote storage URI (e.g., an 's3://' URI). For more information"
168      " about supported remote URIs for model artifacts, see"
169      " https://mlflow.org/docs/latest/tracking.html"
170      "#artifact-stores",
171  )
172  @click.option(
173      "--flavor",
174      "-f",
175      help="Which flavor to be deployed. This will be auto inferred if it's not given",
176  )
177  def update_deployment(flavor, model_uri, target, name, config, endpoint):
178      """
179      Update the deployment with ID `deployment_id` in the specified target.
180      You can update the URI of the model and/or the flavor of the deployed model (in which case the
181      model URI must also be specified).
182  
183      Additional plugin-specific arguments may also be passed to this command, via `-C key=value`.
184      """
185      config_dict = _user_args_to_dict(config)
186      client = interface.get_deploy_client(target)
187  
188      sig = signature(client.update_deployment)
189      if "endpoint" in sig.parameters:
190          ret = client.update_deployment(
191              name, model_uri=model_uri, flavor=flavor, config=config_dict, endpoint=endpoint
192          )
193      else:
194          ret = client.update_deployment(name, model_uri=model_uri, flavor=flavor, config=config_dict)
195      click.echo("Deployment {} is updated (with flavor {})".format(name, ret["flavor"]))
196  
197  
198  @commands.command("delete")
199  @mlflow_mcp(tool_name="delete_deployment")
200  @optional_endpoint_param
201  @parse_custom_arguments
202  @deployment_name
203  @target_details
204  def delete_deployment(target, name, config, endpoint):
205      """
206      Delete the deployment with name given at `--name` from the specified target.
207      """
208      client = interface.get_deploy_client(target)
209  
210      sig = signature(client.delete_deployment)
211      if "config" in sig.parameters:
212          config_dict = _user_args_to_dict(config)
213          if "endpoint" in sig.parameters:
214              client.delete_deployment(name, config=config_dict, endpoint=endpoint)
215          else:
216              client.delete_deployment(name, config=config_dict)
217      else:
218          if "endpoint" in sig.parameters:
219              client.delete_deployment(name, endpoint=endpoint)
220          else:
221              client.delete_deployment(name)
222  
223      click.echo(f"Deployment {name} is deleted")
224  
225  
226  @commands.command("list")
227  @mlflow_mcp(tool_name="list_deployments")
228  @optional_endpoint_param
229  @target_details
230  def list_deployment(target, endpoint):
231      """
232      List the names of all model deployments in the specified target. These names can be used with
233      the `delete`, `update`, and `get` commands.
234      """
235      client = interface.get_deploy_client(target)
236  
237      sig = signature(client.list_deployments)
238      if "endpoint" in sig.parameters:
239          ids = client.list_deployments(endpoint=endpoint)
240      else:
241          ids = client.list_deployments()
242      click.echo(f"List of all deployments:\n{ids}")
243  
244  
245  @commands.command("get")
246  @mlflow_mcp(tool_name="get_deployment")
247  @optional_endpoint_param
248  @deployment_name
249  @target_details
250  def get_deployment(target, name, endpoint):
251      """
252      Print a detailed description of the deployment with name given at ``--name`` in the specified
253      target.
254      """
255      client = interface.get_deploy_client(target)
256  
257      sig = signature(client.get_deployment)
258      if "endpoint" in sig.parameters:
259          desc = client.get_deployment(name, endpoint=endpoint)
260      else:
261          desc = client.get_deployment(name)
262      for key, val in desc.items():
263          click.echo(f"{key}: {val}")
264      click.echo("\n")
265  
266  
267  @commands.command("help")
268  @target_details
269  def target_help(target):
270      """
271      Display additional help for a specific deployment target, e.g. info on target-specific config
272      options and the target's URI format.
273      """
274      click.echo(interface._target_help(target))
275  
276  
277  @commands.command("run-local")
278  @mlflow_mcp(tool_name="run_deployment_locally")
279  @parse_custom_arguments
280  @deployment_name
281  @target_details
282  @cli_args.MODEL_URI
283  @click.option(
284      "--flavor",
285      "-f",
286      help="Which flavor to be deployed. This will be auto inferred if it's not given",
287  )
288  def run_local(flavor, model_uri, target, name, config):
289      """
290      Deploy the model locally. This has very similar signature to ``create`` API
291      """
292      config_dict = _user_args_to_dict(config)
293      interface.run_local(target, name, model_uri, flavor, config_dict)
294  
295  
296  def predictions_to_json(raw_predictions, output):
297      predictions = _get_jsonable_obj(raw_predictions, pandas_orient="records")
298      json.dump(predictions, output, cls=NumpyEncoder)
299  
300  
301  @commands.command("predict")
302  @mlflow_mcp(tool_name="predict_with_deployment")
303  @click.option(
304      "--name",
305      "name",
306      help="Name of the deployment. Exactly one of --name or --endpoint must be specified.",
307  )
308  @click.option(
309      "--endpoint",
310      help="Name of the endpoint. Exactly one of --name or --endpoint must be specified.",
311  )
312  @target_details
313  @parse_input
314  @parse_output
315  def predict(target, name, input_path, output_path, endpoint):
316      """
317      Predict the results for the deployed model for the given input(s)
318      """
319      import pandas as pd
320  
321      if (name, endpoint).count(None) != 1:
322          raise click.UsageError("Must specify exactly one of --name or --endpoint.")
323  
324      df = pd.read_json(input_path)
325      client = interface.get_deploy_client(target)
326  
327      sig = signature(client.predict)
328      if "endpoint" in sig.parameters:
329          result = client.predict(name, df, endpoint=endpoint)
330      else:
331          result = client.predict(name, df)
332      if output_path is not None:
333          result.to_json(output_path)
334      else:
335          click.echo(result.to_json())
336  
337  
338  @commands.command("explain")
339  @mlflow_mcp(tool_name="explain_deployment")
340  @click.option(
341      "--name",
342      "name",
343      help="Name of the deployment. Exactly one of --name or --endpoint must be specified.",
344  )
345  @click.option(
346      "--endpoint",
347      help="Name of the endpoint. Exactly one of --name or --endpoint must be specified.",
348  )
349  @target_details
350  @parse_input
351  @parse_output
352  def explain(target, name, input_path, output_path, endpoint):
353      """
354      Generate explanations of model predictions on the specified input for
355      the deployed model for the given input(s). Explanation output formats vary
356      by deployment target, and can include details like feature importance for
357      understanding/debugging predictions. Run `mlflow deployments help` or
358      consult the documentation for your plugin for details on explanation format.
359      For information about the input data formats accepted by this function,
360      see the following documentation:
361      https://www.mlflow.org/docs/latest/models.html#built-in-deployment-tools
362      """
363      import pandas as pd
364  
365      if (name, endpoint).count(None) != 1:
366          raise click.UsageError("Must specify exactly one of --name or --endpoint.")
367  
368      df = pd.read_json(input_path)
369      client = interface.get_deploy_client(target)
370  
371      sig = signature(client.explain)
372      if "endpoint" in sig.parameters:
373          result = client.explain(name, df, endpoint=endpoint)
374      else:
375          result = client.explain(name, df)
376      if output_path:
377          with open(output_path, "w") as fp:
378              predictions_to_json(result, fp)
379      else:
380          predictions_to_json(result, sys.stdout)
381  
382  
383  @commands.command("create-endpoint")
384  @mlflow_mcp(tool_name="create_deployment_endpoint")
385  @click.option(
386      "--config",
387      "-C",
388      metavar="NAME=VALUE",
389      multiple=True,
390      help="Extra target-specific config for the endpoint, "
391      "of the form -C name=value. See "
392      "documentation/help for your deployment target for a "
393      "list of supported config options.",
394  )
395  @required_endpoint_param
396  @target_details
397  def create_endpoint(target, name, config):
398      """
399      Create an endpoint with the specified name at the specified target.
400  
401      Additional plugin-specific arguments may also be passed to this command, via `-C key=value`
402      """
403      config_dict = _user_args_to_dict(config)
404      client = interface.get_deploy_client(target)
405      endpoint = client.create_endpoint(name, config=config_dict)
406      click.echo("\nEndpoint {} is created".format(endpoint["name"]))
407  
408  
409  @commands.command("update-endpoint")
410  @mlflow_mcp(tool_name="update_deployment_endpoint")
411  @click.option(
412      "--config",
413      "-C",
414      metavar="NAME=VALUE",
415      multiple=True,
416      help="Extra target-specific config for the endpoint, "
417      "of the form -C name=value. See "
418      "documentation/help for your deployment target for a "
419      "list of supported config options.",
420  )
421  @required_endpoint_param
422  @target_details
423  def update_endpoint(target, endpoint, config):
424      """
425      Update the specified endpoint at the specified target.
426  
427      Additional plugin-specific arguments may also be passed to this command, via `-C key=value`
428      """
429      config_dict = _user_args_to_dict(config)
430      client = interface.get_deploy_client(target)
431      client.update_endpoint(endpoint, config=config_dict)
432      click.echo(f"\nEndpoint {endpoint} is updated")
433  
434  
435  @commands.command("delete-endpoint")
436  @mlflow_mcp(tool_name="delete_deployment_endpoint")
437  @required_endpoint_param
438  @target_details
439  def delete_endpoint(target, endpoint):
440      """
441      Delete the specified endpoint at the specified target
442      """
443      client = interface.get_deploy_client(target)
444      client.delete_endpoint(endpoint)
445      click.echo(f"\nEndpoint {endpoint} is deleted")
446  
447  
448  @commands.command("list-endpoints")
449  @mlflow_mcp(tool_name="list_deployment_endpoints")
450  @target_details
451  def list_endpoints(target):
452      """
453      List all endpoints at the specified target
454      """
455      client = interface.get_deploy_client(target)
456      ids = client.list_endpoints()
457      click.echo(f"List of all endpoints:\n{ids}")
458  
459  
460  @commands.command("get-endpoint")
461  @mlflow_mcp(tool_name="get_deployment_endpoint")
462  @required_endpoint_param
463  @target_details
464  def get_endpoint(target, endpoint):
465      """
466      Get details for the specified endpoint at the specified target
467      """
468      client = interface.get_deploy_client(target)
469      desc = client.get_endpoint(endpoint)
470      for key, val in desc.items():
471          click.echo(f"{key}: {val}")
472      click.echo("\n")
473  
474  
475  def validate_config_path(_ctx, _param, value):
476      from mlflow.gateway.config import _validate_config
477  
478      try:
479          _validate_config(value)
480          return value
481      except Exception as e:
482          raise click.BadParameter(str(e))