mirror of
https://github.com/bentoml/BentoML.git
synced 2026-10-12 02:11:27 +00:00
refactor(cli): make CLI commands available as modules (#4487)
* feat(cli): make CLI commands avaiable as modules Signed-off-by: Frost Ming <[email protected]> * fix: handle bentoml group Signed-off-by: Frost Ming <[email protected]> * fix: simply aliases Signed-off-by: Frost Ming <[email protected]> * fix: rename bento_command Signed-off-by: Frost Ming <[email protected]> --------- Signed-off-by: Frost Ming <[email protected]>
This commit is contained in:
@@ -41,6 +41,8 @@ clean: ## Clean all generated files
|
||||
@find . -type f -name '*.py[co]' -delete -o -type d -name __pycache__ -delete
|
||||
|
||||
# Docs
|
||||
watch-docs: ## Build and watch documentation
|
||||
pdm run sphinx-autobuild docs/source docs/build/html --watch $(GIT_ROOT)/src/ --ignore "bazel-*"
|
||||
spellcheck-docs: ## Spell check documentation
|
||||
pdm run sphinx-build -b spelling ./docs/source ./docs/build || (echo "Error running spellchecker.. You may need to run 'make install-spellchecker-deps'"; exit 1)
|
||||
OS := $(shell uname)
|
||||
|
||||
+58
-19
@@ -18,7 +18,6 @@ from bentoml_cli.utils import is_valid_bento_tag
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
from click import Context
|
||||
from click import Group
|
||||
from click import Parameter
|
||||
|
||||
from bentoml._internal.bento import BentoStore
|
||||
@@ -60,7 +59,7 @@ def parse_delete_targets_argument_callback(
|
||||
return delete_targets
|
||||
|
||||
|
||||
def add_bento_management_commands(cli: Group):
|
||||
def bento_management_commands() -> click.Group:
|
||||
import bentoml
|
||||
from bentoml import Tag
|
||||
from bentoml._internal.bento.bento import DEFAULT_BENTO_BUILD_FILE
|
||||
@@ -72,11 +71,14 @@ def add_bento_management_commands(cli: Group):
|
||||
from bentoml._internal.utils import resolve_user_filepath
|
||||
from bentoml._internal.utils import rich_console as console
|
||||
from bentoml.bentos import import_bento
|
||||
from bentoml_cli.utils import BentoMLCommandGroup
|
||||
|
||||
bento_store = BentoMLContainer.bento_store.get()
|
||||
cloud_client = BentoMLContainer.bentocloud_client.get()
|
||||
@click.group(cls=BentoMLCommandGroup)
|
||||
def bentos():
|
||||
"""Commands for managing Bento bundles."""
|
||||
pass
|
||||
|
||||
@cli.command()
|
||||
@bentos.command()
|
||||
@click.argument("bento_tag", type=click.STRING)
|
||||
@click.option(
|
||||
"-o",
|
||||
@@ -84,7 +86,12 @@ def add_bento_management_commands(cli: Group):
|
||||
type=click.Choice(["json", "yaml", "path"]),
|
||||
default="yaml",
|
||||
)
|
||||
def get(bento_tag: str, output: str) -> None: # type: ignore (not accessed)
|
||||
@inject
|
||||
def get(
|
||||
bento_tag: str,
|
||||
output: str,
|
||||
bento_store: BentoStore = Provide[BentoMLContainer.bento_store],
|
||||
) -> None: # type: ignore (not accessed)
|
||||
"""Print Bento details by providing the bento_tag.
|
||||
|
||||
\b
|
||||
@@ -102,7 +109,7 @@ def add_bento_management_commands(cli: Group):
|
||||
info = yaml.dump(bento.info, indent=2, sort_keys=False)
|
||||
console.print(Syntax(info, "yaml", background_color="default"))
|
||||
|
||||
@cli.command(name="list")
|
||||
@bentos.command(name="list")
|
||||
@click.argument("bento_name", type=click.STRING, required=False)
|
||||
@click.option(
|
||||
"-o",
|
||||
@@ -110,7 +117,12 @@ def add_bento_management_commands(cli: Group):
|
||||
type=click.Choice(["json", "yaml", "table"]),
|
||||
default="table",
|
||||
)
|
||||
def list_bentos(bento_name: str, output: str) -> None: # type: ignore (not accessed)
|
||||
@inject
|
||||
def list_bentos(
|
||||
bento_name: str,
|
||||
output: str,
|
||||
bento_store: BentoStore = Provide[BentoMLContainer.bento_store],
|
||||
) -> None: # type: ignore (not accessed)
|
||||
"""List Bentos in local store
|
||||
|
||||
\b
|
||||
@@ -141,7 +153,7 @@ def add_bento_management_commands(cli: Group):
|
||||
info = json.dumps(res, indent=2)
|
||||
console.print(info)
|
||||
elif output == "yaml":
|
||||
info = yaml.safe_dump(res, indent=2)
|
||||
info = t.cast(str, yaml.safe_dump(res, indent=2))
|
||||
console.print(Syntax(info, "yaml", background_color="default"))
|
||||
else:
|
||||
table = Table(box=None)
|
||||
@@ -158,7 +170,7 @@ def add_bento_management_commands(cli: Group):
|
||||
)
|
||||
console.print(table)
|
||||
|
||||
@cli.command()
|
||||
@bentos.command()
|
||||
@click.argument(
|
||||
"delete_targets",
|
||||
nargs=-1,
|
||||
@@ -172,7 +184,12 @@ def add_bento_management_commands(cli: Group):
|
||||
is_flag=True,
|
||||
help="Skip confirmation when deleting a specific bento bundle",
|
||||
)
|
||||
def delete(delete_targets: list[str], yes: bool) -> None: # type: ignore (not accessed)
|
||||
@inject
|
||||
def delete(
|
||||
delete_targets: list[str],
|
||||
yes: bool,
|
||||
bento_store: BentoStore = Provide[BentoMLContainer.bento_store],
|
||||
) -> None: # type: ignore (not accessed)
|
||||
"""Delete Bento in local bento store.
|
||||
|
||||
\b
|
||||
@@ -205,7 +222,7 @@ def add_bento_management_commands(cli: Group):
|
||||
for target in delete_targets:
|
||||
delete_target(target)
|
||||
|
||||
@cli.command()
|
||||
@bentos.command()
|
||||
@click.argument("bento_tag", type=click.STRING)
|
||||
@click.argument(
|
||||
"out_path",
|
||||
@@ -213,7 +230,12 @@ def add_bento_management_commands(cli: Group):
|
||||
default="",
|
||||
required=False,
|
||||
)
|
||||
def export(bento_tag: str, out_path: str) -> None: # type: ignore (not accessed)
|
||||
@inject
|
||||
def export(
|
||||
bento_tag: str,
|
||||
out_path: str,
|
||||
bento_store: BentoStore = Provide[BentoMLContainer.bento_store],
|
||||
) -> None: # type: ignore (not accessed)
|
||||
"""Export a Bento to an external file archive
|
||||
|
||||
\b
|
||||
@@ -235,7 +257,7 @@ def add_bento_management_commands(cli: Group):
|
||||
out_path = bento.export(out_path)
|
||||
click.echo(f"{bento} exported to {out_path}.")
|
||||
|
||||
@cli.command(name="import")
|
||||
@bentos.command(name="import")
|
||||
@click.argument("bento_path", type=click.STRING)
|
||||
def import_bento_(bento_path: str) -> None: # type: ignore (not accessed)
|
||||
"""Import a previously exported Bento archive file
|
||||
@@ -252,7 +274,7 @@ def add_bento_management_commands(cli: Group):
|
||||
bento = import_bento(bento_path)
|
||||
click.echo(f"{bento} imported.")
|
||||
|
||||
@cli.command()
|
||||
@bentos.command()
|
||||
@click.argument("bento_tag", type=click.STRING)
|
||||
@click.option(
|
||||
"-f",
|
||||
@@ -262,13 +284,19 @@ def add_bento_management_commands(cli: Group):
|
||||
help="Force pull from remote Bento store to local and overwrite even if it already exists in local",
|
||||
)
|
||||
@click.pass_obj
|
||||
def pull(shared_options: SharedOptions, bento_tag: str, force: bool) -> None: # type: ignore (not accessed)
|
||||
@inject
|
||||
def pull(
|
||||
shared_options: SharedOptions,
|
||||
bento_tag: str,
|
||||
force: bool,
|
||||
cloud_client: BentoCloudClient = Provide[BentoMLContainer.bentocloud_client],
|
||||
) -> None: # type: ignore (not accessed)
|
||||
"""Pull Bento from a remote Bento store server."""
|
||||
cloud_client.pull_bento(
|
||||
bento_tag, force=force, context=shared_options.cloud_context
|
||||
)
|
||||
|
||||
@cli.command()
|
||||
@bentos.command()
|
||||
@click.argument("bento_tag", type=click.STRING)
|
||||
@click.option(
|
||||
"-f",
|
||||
@@ -284,8 +312,14 @@ def add_bento_management_commands(cli: Group):
|
||||
help="Number of threads to use for upload",
|
||||
)
|
||||
@click.pass_obj
|
||||
@inject
|
||||
def push(
|
||||
shared_options: SharedOptions, bento_tag: str, force: bool, threads: int
|
||||
shared_options: SharedOptions,
|
||||
bento_tag: str,
|
||||
force: bool,
|
||||
threads: int,
|
||||
bento_store: BentoStore = Provide[BentoMLContainer.bento_store],
|
||||
cloud_client: BentoCloudClient = Provide[BentoMLContainer.bentocloud_client],
|
||||
) -> None: # type: ignore (not accessed)
|
||||
"""Push Bento to a remote Bento store server."""
|
||||
bento_obj = bento_store.get(bento_tag)
|
||||
@@ -298,7 +332,7 @@ def add_bento_management_commands(cli: Group):
|
||||
context=shared_options.cloud_context,
|
||||
)
|
||||
|
||||
@cli.command()
|
||||
@bentos.command()
|
||||
@click.argument("build_ctx", type=click.Path(), default=".")
|
||||
@click.option(
|
||||
"-f",
|
||||
@@ -429,3 +463,8 @@ def add_bento_management_commands(cli: Group):
|
||||
bentoml.container.build(bento.tag, backend=backend)
|
||||
|
||||
return bento
|
||||
|
||||
return bentos
|
||||
|
||||
|
||||
bento_command = bento_management_commands()
|
||||
|
||||
+20
-19
@@ -5,19 +5,19 @@ import importlib.metadata
|
||||
import click
|
||||
import psutil
|
||||
|
||||
from bentoml_cli.bentos import add_bento_management_commands
|
||||
from bentoml_cli.cloud import add_cloud_command
|
||||
from bentoml_cli.containerize import add_containerize_command
|
||||
from bentoml_cli.deployment import add_deployment_command
|
||||
from bentoml_cli.env import add_env_command
|
||||
from bentoml_cli.models import add_model_management_commands
|
||||
from bentoml_cli.serve import add_serve_command
|
||||
from bentoml_cli.start import add_start_command
|
||||
from bentoml_cli.utils import BentoMLCommandGroup
|
||||
|
||||
|
||||
def create_bentoml_cli() -> click.Group:
|
||||
def create_bentoml_cli() -> click.Command:
|
||||
from bentoml._internal.context import component_context
|
||||
from bentoml_cli.bentos import bento_command
|
||||
from bentoml_cli.cloud import cloud_command
|
||||
from bentoml_cli.containerize import containerize_command
|
||||
from bentoml_cli.deployment import deploy_command
|
||||
from bentoml_cli.deployment import deployment_command
|
||||
from bentoml_cli.env import env_command
|
||||
from bentoml_cli.models import model_command
|
||||
from bentoml_cli.serve import serve_command
|
||||
from bentoml_cli.start import start_command
|
||||
from bentoml_cli.utils import BentoMLCommandGroup
|
||||
|
||||
component_context.component_type = "cli"
|
||||
|
||||
@@ -37,14 +37,15 @@ def create_bentoml_cli() -> click.Group:
|
||||
"""
|
||||
|
||||
# Add top-level CLI commands
|
||||
add_env_command(bentoml_cli)
|
||||
add_cloud_command(bentoml_cli)
|
||||
add_bento_management_commands(bentoml_cli)
|
||||
add_model_management_commands(bentoml_cli)
|
||||
add_start_command(bentoml_cli)
|
||||
add_serve_command(bentoml_cli)
|
||||
add_containerize_command(bentoml_cli)
|
||||
add_deployment_command(bentoml_cli)
|
||||
bentoml_cli.add_command(env_command)
|
||||
bentoml_cli.add_command(cloud_command)
|
||||
bentoml_cli.add_command(model_command)
|
||||
bentoml_cli.add_subcommands(bento_command)
|
||||
bentoml_cli.add_subcommands(start_command)
|
||||
bentoml_cli.add_subcommands(serve_command)
|
||||
bentoml_cli.add_command(containerize_command)
|
||||
bentoml_cli.add_command(deploy_command)
|
||||
bentoml_cli.add_command(deployment_command)
|
||||
|
||||
if psutil.WINDOWS:
|
||||
import sys
|
||||
|
||||
+72
-73
@@ -6,93 +6,92 @@ import typing as t
|
||||
import click
|
||||
import click_option_group as cog
|
||||
|
||||
from bentoml._internal.cloud.client import RestApiClient
|
||||
from bentoml._internal.cloud.config import CloudClientConfig
|
||||
from bentoml._internal.cloud.config import CloudClientContext
|
||||
from bentoml._internal.cloud.config import add_context
|
||||
from bentoml._internal.cloud.config import default_context_name
|
||||
from bentoml._internal.utils import bentoml_cattr
|
||||
from bentoml.exceptions import CLIException
|
||||
from bentoml_cli.utils import BentoMLCommandGroup
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
from .utils import SharedOptions
|
||||
|
||||
|
||||
def add_cloud_command(cli: click.Group) -> click.Group:
|
||||
from bentoml._internal.cloud.client import RestApiClient
|
||||
from bentoml._internal.cloud.config import CloudClientConfig
|
||||
from bentoml._internal.cloud.config import CloudClientContext
|
||||
from bentoml._internal.cloud.config import add_context
|
||||
from bentoml._internal.cloud.config import default_context_name
|
||||
from bentoml._internal.utils import bentoml_cattr
|
||||
from bentoml.exceptions import CLIException
|
||||
from bentoml_cli.utils import BentoMLCommandGroup
|
||||
@click.group(name="cloud", cls=BentoMLCommandGroup)
|
||||
def cloud_command():
|
||||
"""BentoCloud Subcommands Groups."""
|
||||
|
||||
@cli.group(name="cloud", cls=BentoMLCommandGroup)
|
||||
def cloud():
|
||||
"""BentoCloud Subcommands Groups."""
|
||||
|
||||
@cloud.command()
|
||||
@cog.optgroup.group(
|
||||
"Login", help="Required login options", cls=cog.RequiredAllOptionGroup
|
||||
@cloud_command.command()
|
||||
@cog.optgroup.group(
|
||||
"Login", help="Required login options", cls=cog.RequiredAllOptionGroup
|
||||
)
|
||||
@cog.optgroup.option(
|
||||
"--endpoint",
|
||||
type=click.STRING,
|
||||
help="BentoCloud or Yatai endpoint, i.e: https://cloud.bentoml.com",
|
||||
)
|
||||
@cog.optgroup.option(
|
||||
"--api-token",
|
||||
type=click.STRING,
|
||||
help="BentoCloud or Yatai user API token",
|
||||
)
|
||||
@click.pass_obj
|
||||
def login(shared_options: SharedOptions, endpoint: str, api_token: str) -> None: # type: ignore (not accessed)
|
||||
"""Login to BentoCloud or Yatai server."""
|
||||
cloud_rest_client = RestApiClient(endpoint, api_token)
|
||||
user = cloud_rest_client.v1.get_current_user()
|
||||
|
||||
if user is None:
|
||||
raise CLIException("current user is not found")
|
||||
|
||||
org = cloud_rest_client.v1.get_current_organization()
|
||||
|
||||
if org is None:
|
||||
raise CLIException("current organization is not found")
|
||||
|
||||
ctx = CloudClientContext(
|
||||
name=shared_options.cloud_context
|
||||
if shared_options.cloud_context is not None
|
||||
else default_context_name,
|
||||
endpoint=endpoint,
|
||||
api_token=api_token,
|
||||
email=user.email,
|
||||
)
|
||||
@cog.optgroup.option(
|
||||
"--endpoint",
|
||||
type=click.STRING,
|
||||
help="BentoCloud or Yatai endpoint, i.e: https://cloud.bentoml.com",
|
||||
)
|
||||
@cog.optgroup.option(
|
||||
"--api-token",
|
||||
type=click.STRING,
|
||||
help="BentoCloud or Yatai user API token",
|
||||
)
|
||||
@click.pass_obj
|
||||
def login(shared_options: SharedOptions, endpoint: str, api_token: str) -> None: # type: ignore (not accessed)
|
||||
"""Login to BentoCloud or Yatai server."""
|
||||
cloud_rest_client = RestApiClient(endpoint, api_token)
|
||||
user = cloud_rest_client.v1.get_current_user()
|
||||
|
||||
if user is None:
|
||||
raise CLIException("current user is not found")
|
||||
add_context(ctx)
|
||||
click.echo(f"Successfully logged in to Cloud for {user.name} in {org.name}")
|
||||
|
||||
org = cloud_rest_client.v1.get_current_organization()
|
||||
|
||||
if org is None:
|
||||
raise CLIException("current organization is not found")
|
||||
|
||||
ctx = CloudClientContext(
|
||||
name=(
|
||||
shared_options.cloud_context
|
||||
if shared_options.cloud_context is not None
|
||||
else default_context_name
|
||||
@cloud_command.command()
|
||||
def current_context() -> None: # type: ignore (not accessed)
|
||||
"""Get current cloud context."""
|
||||
click.echo(
|
||||
json.dumps(
|
||||
bentoml_cattr.unstructure(
|
||||
CloudClientConfig.get_config().get_current_context()
|
||||
),
|
||||
endpoint=endpoint,
|
||||
api_token=api_token,
|
||||
email=user.email,
|
||||
indent=2,
|
||||
)
|
||||
)
|
||||
|
||||
add_context(ctx)
|
||||
click.echo(f"Successfully logged in to Cloud for {user.name} in {org.name}")
|
||||
|
||||
@cloud.command()
|
||||
def current_context() -> None: # type: ignore (not accessed)
|
||||
"""Get current cloud context."""
|
||||
click.echo(
|
||||
json.dumps(
|
||||
bentoml_cattr.unstructure(
|
||||
CloudClientConfig.get_config().get_current_context()
|
||||
),
|
||||
indent=2,
|
||||
)
|
||||
@cloud_command.command()
|
||||
def list_context() -> None: # type: ignore (not accessed)
|
||||
"""List all available context."""
|
||||
config = CloudClientConfig.get_config()
|
||||
click.echo(
|
||||
json.dumps(
|
||||
bentoml_cattr.unstructure([i.name for i in config.contexts]), indent=2
|
||||
)
|
||||
)
|
||||
|
||||
@cloud.command()
|
||||
def list_context() -> None: # type: ignore (not accessed)
|
||||
"""List all available context."""
|
||||
config = CloudClientConfig.get_config()
|
||||
click.echo(
|
||||
json.dumps(
|
||||
bentoml_cattr.unstructure([i.name for i in config.contexts]), indent=2
|
||||
)
|
||||
)
|
||||
|
||||
@cloud.command()
|
||||
@click.argument("context_name", type=click.STRING)
|
||||
def update_current_context(context_name: str) -> None: # type: ignore (not accessed)
|
||||
"""Update current context"""
|
||||
ctx = CloudClientConfig.get_config().set_current_context(context_name)
|
||||
click.echo(f"Successfully switched to context: {ctx.name}")
|
||||
|
||||
return cli
|
||||
@cloud_command.command()
|
||||
@click.argument("context_name", type=click.STRING)
|
||||
def update_current_context(context_name: str) -> None: # type: ignore (not accessed)
|
||||
"""Update current context"""
|
||||
ctx = CloudClientConfig.get_config().set_current_context(context_name)
|
||||
click.echo(f"Successfully switched to context: {ctx.name}")
|
||||
|
||||
+235
-241
@@ -9,10 +9,24 @@ import tempfile
|
||||
import typing as t
|
||||
from functools import partial
|
||||
|
||||
import click
|
||||
from click_option_group import optgroup
|
||||
|
||||
from bentoml import container
|
||||
from bentoml._internal.configuration import get_debug_mode
|
||||
from bentoml._internal.container import FEATURES
|
||||
from bentoml._internal.container import REGISTERED_BACKENDS
|
||||
from bentoml._internal.container import determine_container_tag
|
||||
from bentoml._internal.container import enable_buildkit
|
||||
from bentoml.exceptions import BentoMLException
|
||||
from bentoml_cli.utils import kwargs_transformers
|
||||
from bentoml_cli.utils import normalize_none_type
|
||||
from bentoml_cli.utils import opt_callback
|
||||
from bentoml_cli.utils import validate_container_tag
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
from click import Command
|
||||
from click import Context
|
||||
from click import Group
|
||||
from click import Parameter
|
||||
|
||||
from bentoml._internal.container import DefaultBuilder
|
||||
@@ -368,252 +382,232 @@ def buildx_options_group(f: F[t.Any]):
|
||||
return f
|
||||
|
||||
|
||||
def add_containerize_command(cli: Group) -> None:
|
||||
import click
|
||||
from click_option_group import optgroup
|
||||
@click.command(name="containerize")
|
||||
@click.argument("bento_tag", type=click.STRING, metavar="BENTO:TAG")
|
||||
@click.option(
|
||||
"-t",
|
||||
"--image-tag",
|
||||
"--docker-image-tag",
|
||||
metavar="NAME:TAG",
|
||||
help="Name and optionally a tag (format: ``name:tag``) for building container, defaults to the bento tag.",
|
||||
required=False,
|
||||
callback=validate_container_tag,
|
||||
multiple=True,
|
||||
)
|
||||
@click.option(
|
||||
"--backend",
|
||||
help="Define builder backend. Available: {}".format(
|
||||
", ".join(map(lambda s: f"``{s}``", REGISTERED_BACKENDS))
|
||||
),
|
||||
required=False,
|
||||
default="docker",
|
||||
envvar="BENTOML_CONTAINERIZE_BACKEND",
|
||||
type=click.STRING,
|
||||
)
|
||||
@click.option(
|
||||
"--enable-features",
|
||||
multiple=True,
|
||||
nargs=1,
|
||||
metavar="FEATURE[,FEATURE]",
|
||||
help="Enable additional BentoML features. Available features are: {}.".format(
|
||||
", ".join(map(lambda s: f"``{s}``", FEATURES))
|
||||
),
|
||||
)
|
||||
@click.option(
|
||||
"--opt",
|
||||
help="Define options for given backend. (format: ``--opt target=foo --opt build-arg=foo=BAR --opt iidfile:/path/to/file --opt no-cache``)",
|
||||
required=False,
|
||||
multiple=True,
|
||||
callback=opt_callback,
|
||||
metavar="ARG=VALUE[,ARG=VALUE]",
|
||||
)
|
||||
@click.option(
|
||||
"--run-as-root",
|
||||
help="Whether to run backend with as root.",
|
||||
is_flag=True,
|
||||
required=False,
|
||||
default=False,
|
||||
type=bool,
|
||||
)
|
||||
@optgroup.group(
|
||||
"Equivalent options", help="Equivalent option group for backward compatibility"
|
||||
)
|
||||
@compatible_options_group
|
||||
@optgroup.group("Buildx options", help="[Only available with '--backend=buildx']")
|
||||
@buildx_options_group
|
||||
@kwargs_transformers(transformer=normalize_none_type)
|
||||
def containerize_command( # type: ignore
|
||||
bento_tag: str,
|
||||
image_tag: tuple[str] | None,
|
||||
backend: DefaultBuilder,
|
||||
enable_features: tuple[str] | None,
|
||||
run_as_root: bool,
|
||||
_memoized: dict[str, t.Any],
|
||||
**kwargs: t.Any, # pylint: disable=unused-argument
|
||||
) -> None:
|
||||
"""Containerizes given Bento into an OCI-compliant container, with any given OCI builder.
|
||||
|
||||
from bentoml import container
|
||||
from bentoml._internal.configuration import get_debug_mode
|
||||
from bentoml._internal.container import FEATURES
|
||||
from bentoml._internal.container import REGISTERED_BACKENDS
|
||||
from bentoml._internal.container import determine_container_tag
|
||||
from bentoml._internal.container import enable_buildkit
|
||||
from bentoml.exceptions import BentoMLException
|
||||
from bentoml_cli.utils import kwargs_transformers
|
||||
from bentoml_cli.utils import normalize_none_type
|
||||
from bentoml_cli.utils import opt_callback
|
||||
from bentoml_cli.utils import validate_container_tag
|
||||
\b
|
||||
|
||||
@cli.command()
|
||||
@click.argument("bento_tag", type=click.STRING, metavar="BENTO:TAG")
|
||||
@click.option(
|
||||
"-t",
|
||||
"--image-tag",
|
||||
"--docker-image-tag",
|
||||
metavar="NAME:TAG",
|
||||
help="Name and optionally a tag (format: ``name:tag``) for building container, defaults to the bento tag.",
|
||||
required=False,
|
||||
callback=validate_container_tag,
|
||||
multiple=True,
|
||||
``BENTO`` is the target BentoService to be containerized, referenced by its name
|
||||
and version in format of name:version. For example: ``iris_classifier:v1.2.0``
|
||||
|
||||
\b
|
||||
``bentoml containerize`` command also supports the use of the ``latest`` tag
|
||||
which will automatically use the last built version of your Bento.
|
||||
|
||||
\b
|
||||
You can provide a tag for the image built by Bento using the
|
||||
``--image-tag`` flag.
|
||||
|
||||
\b
|
||||
You can also prefixing the tag with a hostname for the repository you wish to push to.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ bentoml containerize iris_classifier:latest -t repo-address.com:username/iris
|
||||
|
||||
This would build an image called ``username/iris:latest`` and push it to docker repository at ``repo-address.com``.
|
||||
|
||||
\b
|
||||
By default, the ``containerize`` command will use the current credentials provided by each backend implementation.
|
||||
|
||||
\b
|
||||
``bentoml containerize`` also leverage BuildKit features such as multi-node
|
||||
builds for cross-platform images, more efficient caching, and more secure builds.
|
||||
BuildKit will be enabled by default by each backend implementation. To opt-out of BuildKit, set ``DOCKER_BUILDKIT=0``:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ DOCKER_BUILDKIT=0 bentoml containerize iris_classifier:latest
|
||||
|
||||
\b
|
||||
``bentoml containerize`` is backend-agnostic, meaning that it can use any OCI-compliant builder to build the container image.
|
||||
By default, it will use the ``docker`` backend, which will use the local Docker daemon to build the image.
|
||||
You can use the ``--backend`` flag to specify a different backend. Note that backend flags will now be unified via ``--opt``.
|
||||
|
||||
``--opt`` accepts multiple flags, and it works with all backend options.
|
||||
|
||||
\b
|
||||
To pass a boolean flag (e.g: ``--no-cache``), use ``--opt no-cache``.
|
||||
|
||||
\b
|
||||
To pass a key-value pair (e.g: ``--build-arg FOO=BAR``), use ``--opt build-arg=FOO=BAR`` or ``--opt build-arg:FOO=BAR``. Make sure to quote the value if it contains spaces.
|
||||
|
||||
\b
|
||||
To pass a argument with one value (path, integer, etc) (e.g: ``--cgroup-parent cgroupv2``), use ``--opt cgroup-parent=cgroupv2`` or ``--opt cgroup-parent:cgroupv2``.
|
||||
"""
|
||||
|
||||
# we can filter out all options that are None or False.
|
||||
_memoized = {k: v for k, v in _memoized.items() if v}
|
||||
logger.debug("Memoized: %s", _memoized)
|
||||
|
||||
# Run healthcheck before containerizing
|
||||
# build will also run healthcheck, but we want to fail early.
|
||||
try:
|
||||
container.health(backend)
|
||||
except subprocess.CalledProcessError:
|
||||
raise BentoMLException(f"Backend {backend} is not healthy")
|
||||
|
||||
# --progress is not available without BuildKit.
|
||||
if not enable_buildkit(backend=backend) and "progress" in _memoized:
|
||||
_memoized.pop("progress")
|
||||
elif get_debug_mode():
|
||||
_memoized["progress"] = "plain"
|
||||
_memoized["_run_as_root"] = run_as_root
|
||||
|
||||
features: tuple[str] | None = None
|
||||
if enable_features:
|
||||
features = tuple(
|
||||
itertools.chain.from_iterable(map(lambda s: s.split(","), enable_features))
|
||||
)
|
||||
result = container.build(
|
||||
bento_tag,
|
||||
backend=backend,
|
||||
image_tag=image_tag,
|
||||
features=features,
|
||||
**_memoized,
|
||||
)
|
||||
@click.option(
|
||||
"--backend",
|
||||
help="Define builder backend. Available: {}".format(
|
||||
", ".join(map(lambda s: f"``{s}``", REGISTERED_BACKENDS))
|
||||
),
|
||||
required=False,
|
||||
default="docker",
|
||||
envvar="BENTOML_CONTAINERIZE_BACKEND",
|
||||
type=click.STRING,
|
||||
)
|
||||
@click.option(
|
||||
"--enable-features",
|
||||
multiple=True,
|
||||
nargs=1,
|
||||
metavar="FEATURE[,FEATURE]",
|
||||
help="Enable additional BentoML features. Available features are: {}.".format(
|
||||
", ".join(map(lambda s: f"``{s}``", FEATURES))
|
||||
),
|
||||
)
|
||||
@click.option(
|
||||
"--opt",
|
||||
help="Define options for given backend. (format: ``--opt target=foo --opt build-arg=foo=BAR --opt iidfile:/path/to/file --opt no-cache``)",
|
||||
required=False,
|
||||
multiple=True,
|
||||
callback=opt_callback,
|
||||
metavar="ARG=VALUE[,ARG=VALUE]",
|
||||
)
|
||||
@click.option(
|
||||
"--run-as-root",
|
||||
help="Whether to run backend with as root.",
|
||||
is_flag=True,
|
||||
required=False,
|
||||
default=False,
|
||||
type=bool,
|
||||
)
|
||||
@optgroup.group(
|
||||
"Equivalent options", help="Equivalent option group for backward compatibility"
|
||||
)
|
||||
@compatible_options_group
|
||||
@optgroup.group("Buildx options", help="[Only available with '--backend=buildx']")
|
||||
@buildx_options_group
|
||||
@kwargs_transformers(transformer=normalize_none_type)
|
||||
def containerize( # type: ignore
|
||||
bento_tag: str,
|
||||
image_tag: tuple[str] | None,
|
||||
backend: DefaultBuilder,
|
||||
enable_features: tuple[str] | None,
|
||||
run_as_root: bool,
|
||||
_memoized: dict[str, t.Any],
|
||||
**kwargs: t.Any, # pylint: disable=unused-argument
|
||||
) -> None:
|
||||
"""Containerizes given Bento into an OCI-compliant container, with any given OCI builder.
|
||||
|
||||
\b
|
||||
if result is not None:
|
||||
tags = determine_container_tag(bento_tag, image_tag=image_tag)
|
||||
|
||||
``BENTO`` is the target BentoService to be containerized, referenced by its name
|
||||
and version in format of name:version. For example: ``iris_classifier:v1.2.0``
|
||||
|
||||
\b
|
||||
``bentoml containerize`` command also supports the use of the ``latest`` tag
|
||||
which will automatically use the last built version of your Bento.
|
||||
|
||||
\b
|
||||
You can provide a tag for the image built by Bento using the
|
||||
``--image-tag`` flag.
|
||||
|
||||
\b
|
||||
You can also prefixing the tag with a hostname for the repository you wish to push to.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ bentoml containerize iris_classifier:latest -t repo-address.com:username/iris
|
||||
|
||||
This would build an image called ``username/iris:latest`` and push it to docker repository at ``repo-address.com``.
|
||||
|
||||
\b
|
||||
By default, the ``containerize`` command will use the current credentials provided by each backend implementation.
|
||||
|
||||
\b
|
||||
``bentoml containerize`` also leverage BuildKit features such as multi-node
|
||||
builds for cross-platform images, more efficient caching, and more secure builds.
|
||||
BuildKit will be enabled by default by each backend implementation. To opt-out of BuildKit, set ``DOCKER_BUILDKIT=0``:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
$ DOCKER_BUILDKIT=0 bentoml containerize iris_classifier:latest
|
||||
|
||||
\b
|
||||
``bentoml containerize`` is backend-agnostic, meaning that it can use any OCI-compliant builder to build the container image.
|
||||
By default, it will use the ``docker`` backend, which will use the local Docker daemon to build the image.
|
||||
You can use the ``--backend`` flag to specify a different backend. Note that backend flags will now be unified via ``--opt``.
|
||||
|
||||
``--opt`` accepts multiple flags, and it works with all backend options.
|
||||
|
||||
\b
|
||||
To pass a boolean flag (e.g: ``--no-cache``), use ``--opt no-cache``.
|
||||
|
||||
\b
|
||||
To pass a key-value pair (e.g: ``--build-arg FOO=BAR``), use ``--opt build-arg=FOO=BAR`` or ``--opt build-arg:FOO=BAR``. Make sure to quote the value if it contains spaces.
|
||||
|
||||
\b
|
||||
To pass a argument with one value (path, integer, etc) (e.g: ``--cgroup-parent cgroupv2``), use ``--opt cgroup-parent=cgroupv2`` or ``--opt cgroup-parent:cgroupv2``.
|
||||
"""
|
||||
|
||||
# we can filter out all options that are None or False.
|
||||
_memoized = {k: v for k, v in _memoized.items() if v}
|
||||
logger.debug("Memoized: %s", _memoized)
|
||||
|
||||
# Run healthcheck before containerizing
|
||||
# build will also run healthcheck, but we want to fail early.
|
||||
try:
|
||||
container.health(backend)
|
||||
except subprocess.CalledProcessError:
|
||||
raise BentoMLException(f"Backend {backend} is not healthy")
|
||||
|
||||
# --progress is not available without BuildKit.
|
||||
if not enable_buildkit(backend=backend) and "progress" in _memoized:
|
||||
_memoized.pop("progress")
|
||||
elif get_debug_mode():
|
||||
_memoized["progress"] = "plain"
|
||||
_memoized["_run_as_root"] = run_as_root
|
||||
|
||||
features: tuple[str] | None = None
|
||||
if enable_features:
|
||||
features = tuple(
|
||||
itertools.chain.from_iterable(
|
||||
map(lambda s: s.split(","), enable_features)
|
||||
container_runtime = backend
|
||||
# This logic determines some of the output messages.
|
||||
if backend == "buildah":
|
||||
if shutil.which("podman") is None:
|
||||
logger.warning(
|
||||
"'buildah' are only used to build the image. To run the image, 'podman' is required to be installed (podman not found under PATH). See https://podman.io/getting-started/installation."
|
||||
)
|
||||
)
|
||||
result = container.build(
|
||||
bento_tag,
|
||||
backend=backend,
|
||||
image_tag=image_tag,
|
||||
features=features,
|
||||
**_memoized,
|
||||
sys.exit(0)
|
||||
else:
|
||||
container_runtime = "podman"
|
||||
elif backend == "buildx":
|
||||
# buildx is a syntatic sugar for docker buildx build
|
||||
container_runtime = "docker"
|
||||
elif backend == "buildctl":
|
||||
if shutil.which("docker") is not None:
|
||||
container_runtime = "docker"
|
||||
elif shutil.which("podman") is not None:
|
||||
container_runtime = "podman"
|
||||
elif shutil.which("nerdctl") is not None:
|
||||
container_runtime = "nerdctl"
|
||||
else:
|
||||
click.echo(
|
||||
click.style(
|
||||
"To load image built with 'buildctl' requires one of "
|
||||
"docker, podman, nerdctl (None are found in PATH). "
|
||||
"Make sure they are visible to PATH and try again.",
|
||||
fg="yellow",
|
||||
),
|
||||
color=True,
|
||||
)
|
||||
sys.exit(0)
|
||||
if "output" not in _memoized:
|
||||
tmp_path = tempfile.gettempdir()
|
||||
type_prefix = "type=oci,name="
|
||||
if container_runtime == "docker":
|
||||
type_prefix = "type=docker,name=docker.io/"
|
||||
click.echo(
|
||||
click.style(
|
||||
"Autoconfig is now deprecated and will be removed "
|
||||
"in the next future release. We recommend setting "
|
||||
"output to a tarfile should you wish to load the "
|
||||
"image locally. For example:",
|
||||
fg="yellow",
|
||||
),
|
||||
color=True,
|
||||
)
|
||||
click.echo(
|
||||
click.style(
|
||||
f" bentoml containerize {bento_tag} --backend buildctl --opt output={type_prefix}{tags[0]},dest={tmp_path}/{tags[0].replace(':', '_')}.tar\n"
|
||||
f" {container_runtime} load -i {tmp_path}/{tags[0].replace(':', '_')}.tar",
|
||||
fg="yellow",
|
||||
),
|
||||
color=True,
|
||||
)
|
||||
o = subprocess.check_output([container_runtime, "load"], input=result)
|
||||
if get_debug_mode():
|
||||
click.echo(o.decode("utf-8").strip())
|
||||
sys.exit(0)
|
||||
return result
|
||||
|
||||
click.echo(
|
||||
f'Successfully built Bento container for "{bento_tag}" with tag(s) "{",".join(tags)}"',
|
||||
)
|
||||
instructions = (
|
||||
"To run your newly built Bento container, run:\n"
|
||||
+ f" {container_runtime} run --rm -p 3000:3000 {tags[0]}\n"
|
||||
)
|
||||
|
||||
if result is not None:
|
||||
tags = determine_container_tag(bento_tag, image_tag=image_tag)
|
||||
|
||||
container_runtime = backend
|
||||
# This logic determines some of the output messages.
|
||||
if backend == "buildah":
|
||||
if shutil.which("podman") is None:
|
||||
logger.warning(
|
||||
"'buildah' are only used to build the image. To run the image, 'podman' is required to be installed (podman not found under PATH). See https://podman.io/getting-started/installation."
|
||||
)
|
||||
sys.exit(0)
|
||||
else:
|
||||
container_runtime = "podman"
|
||||
elif backend == "buildx":
|
||||
# buildx is a syntatic sugar for docker buildx build
|
||||
container_runtime = "docker"
|
||||
elif backend == "buildctl":
|
||||
if shutil.which("docker") is not None:
|
||||
container_runtime = "docker"
|
||||
elif shutil.which("podman") is not None:
|
||||
container_runtime = "podman"
|
||||
elif shutil.which("nerdctl") is not None:
|
||||
container_runtime = "nerdctl"
|
||||
else:
|
||||
click.echo(
|
||||
click.style(
|
||||
"To load image built with 'buildctl' requires one of "
|
||||
"docker, podman, nerdctl (None are found in PATH). "
|
||||
"Make sure they are visible to PATH and try again.",
|
||||
fg="yellow",
|
||||
),
|
||||
color=True,
|
||||
)
|
||||
sys.exit(0)
|
||||
if "output" not in _memoized:
|
||||
tmp_path = tempfile.gettempdir()
|
||||
type_prefix = "type=oci,name="
|
||||
if container_runtime == "docker":
|
||||
type_prefix = "type=docker,name=docker.io/"
|
||||
click.echo(
|
||||
click.style(
|
||||
"Autoconfig is now deprecated and will be removed "
|
||||
"in the next future release. We recommend setting "
|
||||
"output to a tarfile should you wish to load the "
|
||||
"image locally. For example:",
|
||||
fg="yellow",
|
||||
),
|
||||
color=True,
|
||||
)
|
||||
click.echo(
|
||||
click.style(
|
||||
f" bentoml containerize {bento_tag} --backend buildctl --opt output={type_prefix}{tags[0]},dest={tmp_path}/{tags[0].replace(':', '_')}.tar\n"
|
||||
f" {container_runtime} load -i {tmp_path}/{tags[0].replace(':', '_')}.tar",
|
||||
fg="yellow",
|
||||
),
|
||||
color=True,
|
||||
)
|
||||
o = subprocess.check_output(
|
||||
[container_runtime, "load"], input=result
|
||||
)
|
||||
if get_debug_mode():
|
||||
click.echo(o.decode("utf-8").strip())
|
||||
sys.exit(0)
|
||||
return result
|
||||
|
||||
click.echo(
|
||||
f'Successfully built Bento container for "{bento_tag}" with tag(s) "{",".join(tags)}"',
|
||||
if features is not None and any(
|
||||
has_grpc in features
|
||||
for has_grpc in ("all", "grpc", "grpc-reflection", "grpc-channelz")
|
||||
):
|
||||
instructions += (
|
||||
"To serve with gRPC instead, run:\n"
|
||||
+ f" {container_runtime} run --rm -p 3000:3000 -p 3001:3001 {tags[0]} serve-grpc\n"
|
||||
)
|
||||
instructions = (
|
||||
"To run your newly built Bento container, run:\n"
|
||||
+ f" {container_runtime} run --rm -p 3000:3000 {tags[0]}\n"
|
||||
)
|
||||
|
||||
if features is not None and any(
|
||||
has_grpc in features
|
||||
for has_grpc in ("all", "grpc", "grpc-reflection", "grpc-channelz")
|
||||
):
|
||||
instructions += (
|
||||
"To serve with gRPC instead, run:\n"
|
||||
+ f" {container_runtime} run --rm -p 3000:3000 -p 3001:3001 {tags[0]} serve-grpc\n"
|
||||
)
|
||||
click.echo(instructions, nl=False)
|
||||
raise SystemExit(0)
|
||||
raise SystemExit(1)
|
||||
click.echo(instructions, nl=False)
|
||||
raise SystemExit(0)
|
||||
raise SystemExit(1)
|
||||
|
||||
+645
-653
File diff suppressed because it is too large
Load Diff
+55
-56
@@ -12,6 +12,10 @@ from gettext import gettext
|
||||
|
||||
import click
|
||||
|
||||
from bentoml._internal.utils.pkg import PackageNotFoundError
|
||||
from bentoml._internal.utils.pkg import get_pkg_version
|
||||
from bentoml.exceptions import CLIException
|
||||
|
||||
conda_packages_name = "conda_packages"
|
||||
|
||||
_ENVVAR = [
|
||||
@@ -104,67 +108,62 @@ def pretty_format(
|
||||
return "\n".join({"md": format_md, "bash": format_bash}[output](env, info_dict))
|
||||
|
||||
|
||||
def add_env_command(cli: click.Group) -> None:
|
||||
from bentoml._internal.utils.pkg import PackageNotFoundError
|
||||
from bentoml._internal.utils.pkg import get_pkg_version
|
||||
from bentoml.exceptions import CLIException
|
||||
@click.command(help=gettext("Print environment info and exit"), name="env")
|
||||
@click.option(
|
||||
"-o",
|
||||
"--output",
|
||||
type=click.Choice(["md", "bash"]),
|
||||
default="md",
|
||||
show_default=True,
|
||||
help="Output format. '-o bash' to display without format.",
|
||||
)
|
||||
@click.pass_context
|
||||
def env_command(ctx: click.Context, output: t.Literal["md", "bash"]) -> None: # type: ignore (unused warning)
|
||||
if output not in ["md", "bash"]:
|
||||
raise CLIException(f"Unknown output format: {output}")
|
||||
|
||||
@cli.command(help=gettext("Print environment info and exit"))
|
||||
@click.option(
|
||||
"-o",
|
||||
"--output",
|
||||
type=click.Choice(["md", "bash"]),
|
||||
default="md",
|
||||
show_default=True,
|
||||
help="Output format. '-o bash' to display without format.",
|
||||
)
|
||||
@click.pass_context
|
||||
def env(ctx: click.Context, output: t.Literal["md", "bash"]) -> None: # type: ignore (unused warning)
|
||||
if output not in ["md", "bash"]:
|
||||
raise CLIException(f"Unknown output format: {output}")
|
||||
is_windows = sys.platform == "win32"
|
||||
|
||||
is_windows = sys.platform == "win32"
|
||||
info_dict: dict[str, str | list[str]] = {
|
||||
"bentoml": importlib.metadata.version("bentoml"),
|
||||
"python": platform.python_version(),
|
||||
"platform": platform.platform(),
|
||||
}
|
||||
|
||||
info_dict: dict[str, str | list[str]] = {
|
||||
"bentoml": importlib.metadata.version("bentoml"),
|
||||
"python": platform.python_version(),
|
||||
"platform": platform.platform(),
|
||||
}
|
||||
if is_windows:
|
||||
from ctypes import windll
|
||||
|
||||
if is_windows:
|
||||
from ctypes import windll
|
||||
# https://stackoverflow.com/a/1026626
|
||||
is_admin: bool = windll.shell32.IsUserAnAdmin() != 0
|
||||
info_dict["is_window_admin"] = str(is_admin)
|
||||
else:
|
||||
info_dict["uid_gid"] = f"{os.geteuid()}:{os.getegid()}"
|
||||
|
||||
# https://stackoverflow.com/a/1026626
|
||||
is_admin: bool = windll.shell32.IsUserAnAdmin() != 0
|
||||
info_dict["is_window_admin"] = str(is_admin)
|
||||
else:
|
||||
info_dict["uid_gid"] = f"{os.geteuid()}:{os.getegid()}"
|
||||
if "CONDA_PREFIX" in os.environ:
|
||||
# conda packages
|
||||
conda_like = None
|
||||
for possible_exec in ["conda", "mamba", "micromamba"]:
|
||||
if shutil.which(possible_exec) is not None:
|
||||
conda_like = possible_exec
|
||||
break
|
||||
assert (
|
||||
conda_like is not None
|
||||
), "couldn't find a conda-like executable, while CONDA_PREFIX is set."
|
||||
conda_packages = run_cmd([conda_like, "env", "export"])
|
||||
|
||||
if "CONDA_PREFIX" in os.environ:
|
||||
# conda packages
|
||||
conda_like = None
|
||||
for possible_exec in ["conda", "mamba", "micromamba"]:
|
||||
if shutil.which(possible_exec) is not None:
|
||||
conda_like = possible_exec
|
||||
break
|
||||
assert (
|
||||
conda_like is not None
|
||||
), "couldn't find a conda-like executable, while CONDA_PREFIX is set."
|
||||
conda_packages = run_cmd([conda_like, "env", "export"])
|
||||
# user is currently in a conda environment,
|
||||
# doing this is faster than invoking `conda --version`
|
||||
try:
|
||||
conda_version = get_pkg_version("conda")
|
||||
except PackageNotFoundError:
|
||||
conda_version = run_cmd([conda_like, "--version"])[0].split(" ")[-1]
|
||||
|
||||
# user is currently in a conda environment,
|
||||
# doing this is faster than invoking `conda --version`
|
||||
try:
|
||||
conda_version = get_pkg_version("conda")
|
||||
except PackageNotFoundError:
|
||||
conda_version = run_cmd([conda_like, "--version"])[0].split(" ")[-1]
|
||||
info_dict[conda_like] = conda_version
|
||||
info_dict["in_conda_env"] = str(True)
|
||||
info_dict["conda_packages"] = conda_packages
|
||||
|
||||
info_dict[conda_like] = conda_version
|
||||
info_dict["in_conda_env"] = str(True)
|
||||
info_dict["conda_packages"] = conda_packages
|
||||
|
||||
# process info from `pip freeze`
|
||||
pip_packages = run_cmd([sys.executable, "-m", "pip", "freeze"])
|
||||
info_dict["pip_packages"] = pip_packages
|
||||
click.echo(pretty_format(info_dict, output=output))
|
||||
ctx.exit(0)
|
||||
# process info from `pip freeze`
|
||||
pip_packages = run_cmd([sys.executable, "-m", "pip", "freeze"])
|
||||
info_dict["pip_packages"] = pip_packages
|
||||
click.echo(pretty_format(info_dict, output=output))
|
||||
ctx.exit(0)
|
||||
|
||||
+313
-275
@@ -7,16 +7,31 @@ import click
|
||||
import yaml
|
||||
from rich.syntax import Syntax
|
||||
from rich.table import Table
|
||||
from simple_di import Provide
|
||||
from simple_di import inject
|
||||
|
||||
from bentoml import Tag
|
||||
from bentoml._internal.bento.bento import DEFAULT_BENTO_BUILD_FILE
|
||||
from bentoml._internal.bento.build_config import BentoBuildConfig
|
||||
from bentoml._internal.container import BentoMLContainer
|
||||
from bentoml._internal.utils import calc_dir_size
|
||||
from bentoml._internal.utils import human_readable_size
|
||||
from bentoml._internal.utils import resolve_user_filepath
|
||||
from bentoml._internal.utils import rich_console as console
|
||||
from bentoml.exceptions import InvalidArgument
|
||||
from bentoml.models import import_model
|
||||
from bentoml_cli.utils import BentoMLCommandGroup
|
||||
from bentoml_cli.utils import is_valid_bento_name
|
||||
from bentoml_cli.utils import is_valid_bento_tag
|
||||
|
||||
if t.TYPE_CHECKING:
|
||||
from click import Context
|
||||
from click import Group
|
||||
from click import Parameter
|
||||
|
||||
from bentoml._internal.bento import BentoStore
|
||||
from bentoml._internal.cloud import BentoCloudClient
|
||||
from bentoml._internal.models import ModelStore
|
||||
|
||||
from .utils import SharedOptions
|
||||
|
||||
|
||||
@@ -43,282 +58,305 @@ def parse_delete_targets_argument_callback(
|
||||
return delete_targets
|
||||
|
||||
|
||||
def add_model_management_commands(cli: Group) -> None:
|
||||
from bentoml import Tag
|
||||
from bentoml._internal.bento.bento import DEFAULT_BENTO_BUILD_FILE
|
||||
from bentoml._internal.bento.build_config import BentoBuildConfig
|
||||
from bentoml._internal.configuration.containers import BentoMLContainer
|
||||
from bentoml._internal.utils import calc_dir_size
|
||||
from bentoml._internal.utils import human_readable_size
|
||||
from bentoml._internal.utils import resolve_user_filepath
|
||||
from bentoml._internal.utils import rich_console as console
|
||||
from bentoml.exceptions import InvalidArgument
|
||||
from bentoml.models import import_model
|
||||
@click.group(name="models", cls=BentoMLCommandGroup)
|
||||
def model_command():
|
||||
"""Model Subcommands Groups"""
|
||||
|
||||
model_store = BentoMLContainer.model_store.get()
|
||||
cloud_client = BentoMLContainer.bentocloud_client.get()
|
||||
bento_store = BentoMLContainer.bento_store.get()
|
||||
|
||||
@cli.group(name="models", cls=BentoMLCommandGroup)
|
||||
def model_cli():
|
||||
"""Model Subcommands Groups"""
|
||||
@model_command.command()
|
||||
@click.argument("model_tag", type=click.STRING)
|
||||
@click.option(
|
||||
"-o",
|
||||
"--output",
|
||||
type=click.Choice(["json", "yaml", "path"]),
|
||||
default="yaml",
|
||||
)
|
||||
@inject
|
||||
def get(
|
||||
model_tag: str,
|
||||
output: str,
|
||||
model_store: ModelStore = Provide[BentoMLContainer.model_store],
|
||||
) -> None: # type: ignore (not accessed)
|
||||
"""Print Model details by providing the model_tag
|
||||
|
||||
@model_cli.command()
|
||||
@click.argument("model_tag", type=click.STRING)
|
||||
@click.option(
|
||||
"-o",
|
||||
"--output",
|
||||
type=click.Choice(["json", "yaml", "path"]),
|
||||
default="yaml",
|
||||
)
|
||||
def get(model_tag: str, output: str) -> None: # type: ignore (not accessed)
|
||||
"""Print Model details by providing the model_tag
|
||||
\b
|
||||
bentoml get iris_clf:qojf5xauugwqtgxi
|
||||
bentoml get iris_clf:qojf5xauugwqtgxi --output=json
|
||||
"""
|
||||
model = model_store.get(model_tag)
|
||||
|
||||
\b
|
||||
bentoml get iris_clf:qojf5xauugwqtgxi
|
||||
bentoml get iris_clf:qojf5xauugwqtgxi --output=json
|
||||
"""
|
||||
model = model_store.get(model_tag)
|
||||
|
||||
if output == "path":
|
||||
console.print(model.path)
|
||||
elif output == "json":
|
||||
info = json.dumps(model.info.to_dict(), indent=2, default=str)
|
||||
console.print_json(info)
|
||||
else:
|
||||
console.print(
|
||||
Syntax(str(model.info.dump()), "yaml", background_color="default")
|
||||
)
|
||||
|
||||
@model_cli.command(name="list")
|
||||
@click.argument("model_name", type=click.STRING, required=False)
|
||||
@click.option(
|
||||
"-o",
|
||||
"--output",
|
||||
type=click.Choice(["json", "yaml", "table"]),
|
||||
default="table",
|
||||
)
|
||||
def list_models(model_name: str, output: str) -> None: # type: ignore (not accessed)
|
||||
"""List Models in local store
|
||||
|
||||
\b
|
||||
# show all models saved
|
||||
$ bentoml models list
|
||||
|
||||
\b
|
||||
# show all versions of bento with the name FraudDetector
|
||||
$ bentoml models list FraudDetector
|
||||
"""
|
||||
|
||||
models = model_store.list(model_name)
|
||||
res = [
|
||||
{
|
||||
"tag": str(model.tag),
|
||||
"module": model.info.module,
|
||||
"size": human_readable_size(calc_dir_size(model.path)),
|
||||
"creation_time": model.info.creation_time.astimezone().strftime(
|
||||
"%Y-%m-%d %H:%M:%S"
|
||||
),
|
||||
}
|
||||
for model in sorted(
|
||||
models, key=lambda x: x.info.creation_time, reverse=True
|
||||
)
|
||||
]
|
||||
if output == "json":
|
||||
info = json.dumps(res, indent=2)
|
||||
console.print_json(info)
|
||||
elif output == "yaml":
|
||||
info = yaml.safe_dump(res, indent=2)
|
||||
console.print(Syntax(info, "yaml"), background_color="default")
|
||||
else:
|
||||
table = Table(box=None)
|
||||
table.add_column("Tag")
|
||||
table.add_column("Module")
|
||||
table.add_column("Size")
|
||||
table.add_column("Creation Time")
|
||||
for model in res:
|
||||
table.add_row(
|
||||
model["tag"],
|
||||
model["module"],
|
||||
model["size"],
|
||||
model["creation_time"],
|
||||
)
|
||||
console.print(table)
|
||||
|
||||
@model_cli.command()
|
||||
@click.argument(
|
||||
"delete_targets",
|
||||
nargs=-1,
|
||||
callback=parse_delete_targets_argument_callback,
|
||||
required=True,
|
||||
)
|
||||
@click.option(
|
||||
"-y",
|
||||
"--yes",
|
||||
"--assume-yes",
|
||||
is_flag=True,
|
||||
help="Skip confirmation when deleting a specific model",
|
||||
)
|
||||
def delete(delete_targets: str, yes: bool) -> None: # type: ignore (not accessed)
|
||||
"""Delete Model in local model store.
|
||||
|
||||
\b
|
||||
Examples:
|
||||
* Delete single model by "name:version", e.g: `bentoml models delete iris_clf:v1`
|
||||
* Bulk delete all models with a specific name, e.g.: `bentoml models delete iris_clf`
|
||||
* Bulk delete multiple models by name and version, separated by ",", e.g.: `bentoml models delete iris_clf:v1,iris_clf:v2`
|
||||
* Bulk delete multiple models by name and version, separated by " ", e.g.: `bentoml models delete iris_clf:v1 iris_clf:v2`
|
||||
* Bulk delete without confirmation, e.g.: `bentoml models delete IrisClassifier --yes`
|
||||
""" # noqa
|
||||
from bentoml.exceptions import BentoMLException
|
||||
|
||||
def check_model_is_used(tag: Tag) -> None:
|
||||
in_use: list[Tag] = []
|
||||
for bento in bento_store.list():
|
||||
if bento._model_store is not None:
|
||||
continue
|
||||
if any(model.tag == tag for model in bento.info.all_models):
|
||||
in_use.append(bento.tag)
|
||||
if in_use:
|
||||
raise BentoMLException(
|
||||
f"Model {tag} is being used by the following bentos and can't be deleted:\n "
|
||||
+ "\n ".join(map(str, in_use))
|
||||
)
|
||||
|
||||
def delete_target(target: str) -> None:
|
||||
tag = Tag.from_str(target)
|
||||
|
||||
if tag.version is None:
|
||||
to_delete_models = model_store.list(target)
|
||||
else:
|
||||
to_delete_models = [model_store.get(tag)]
|
||||
|
||||
for model in to_delete_models:
|
||||
if yes:
|
||||
delete_confirmed = True
|
||||
else:
|
||||
delete_confirmed = click.confirm(f"delete model {model.tag}?")
|
||||
|
||||
if delete_confirmed:
|
||||
check_model_is_used(model.tag)
|
||||
model_store.delete(model.tag)
|
||||
click.echo(f"{model} deleted.")
|
||||
|
||||
for target in delete_targets:
|
||||
delete_target(target)
|
||||
|
||||
@model_cli.command()
|
||||
@click.argument("model_tag", type=click.STRING)
|
||||
@click.argument("out_path", type=click.STRING, default="", required=False)
|
||||
def export(model_tag: str, out_path: str) -> None: # type: ignore (not accessed)
|
||||
"""Export a Model to an external archive file
|
||||
|
||||
arguments:
|
||||
|
||||
\b
|
||||
MODEL_TAG: model identifier
|
||||
OUT_PATH: output path of exported model.
|
||||
If this argument is not provided, model is exported to name-version.bentomodel in the current directory.
|
||||
Besides native .bentomodel format, we also support formats like tar('tar'), tar.gz ('gz'), tar.xz ('xz'), tar.bz2 ('bz2'), and zip.
|
||||
|
||||
examples:
|
||||
|
||||
\b
|
||||
bentoml models export FraudDetector:latest
|
||||
bentoml models export FraudDetector:latest ./my_model.bentomodel
|
||||
bentoml models export FraudDetector:20210709_DE14C9 ./my_model.bentomodel
|
||||
bentoml models export FraudDetector:20210709_DE14C9 s3://mybucket/models/my_model.bentomodel
|
||||
"""
|
||||
bentomodel = model_store.get(model_tag)
|
||||
out_path = bentomodel.export(out_path)
|
||||
click.echo(f"{bentomodel} exported to {out_path}.")
|
||||
|
||||
@model_cli.command(name="import")
|
||||
@click.argument("model_path", type=click.STRING)
|
||||
def import_from(model_path: str) -> None: # type: ignore (not accessed)
|
||||
"""Import a previously exported Model archive file
|
||||
|
||||
bentoml models import ./my_model.bentomodel
|
||||
bentoml models import s3://mybucket/models/my_model.bentomodel
|
||||
"""
|
||||
bentomodel = import_model(model_path)
|
||||
click.echo(f"{bentomodel} imported.")
|
||||
|
||||
@model_cli.command()
|
||||
@click.argument("model_tag", type=click.STRING, required=False)
|
||||
@click.option(
|
||||
"-f",
|
||||
"--force",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Force pull from remote model store to local and overwrite even if it already exists in local",
|
||||
)
|
||||
@click.option(
|
||||
"-F",
|
||||
"--bentofile",
|
||||
type=click.STRING,
|
||||
default=DEFAULT_BENTO_BUILD_FILE,
|
||||
help="Path to bentofile. Default to 'bentofile.yaml'",
|
||||
)
|
||||
@click.pass_context
|
||||
def pull(ctx: click.Context, model_tag: str | None, force: bool, bentofile: str): # type: ignore (not accessed)
|
||||
"""Pull Model from a remote model store. If model_tag is not provided,
|
||||
it will pull models defined in bentofile.yaml.
|
||||
"""
|
||||
from click.core import ParameterSource
|
||||
|
||||
if model_tag is not None:
|
||||
if ctx.get_parameter_source("bentofile") != ParameterSource.DEFAULT:
|
||||
click.echo("-f bentofile is ignored when model_tag is provided")
|
||||
cloud_client.pull_model(
|
||||
model_tag, force=force, context=ctx.obj.cloud_context
|
||||
)
|
||||
return
|
||||
|
||||
try:
|
||||
bentofile = resolve_user_filepath(bentofile, None)
|
||||
except FileNotFoundError:
|
||||
raise InvalidArgument(f'bentofile "{bentofile}" not found')
|
||||
|
||||
with open(bentofile, "r", encoding="utf-8") as f:
|
||||
build_config = BentoBuildConfig.from_yaml(f)
|
||||
|
||||
if not build_config.models:
|
||||
raise InvalidArgument(
|
||||
"No model to pull, please provide a model tag or define models in bentofile.yaml"
|
||||
)
|
||||
for model_spec in build_config.models:
|
||||
cloud_client.pull_model(
|
||||
model_spec.tag,
|
||||
force=force,
|
||||
context=t.cast("SharedOptions", ctx.obj).cloud_context,
|
||||
query=model_spec.filter,
|
||||
)
|
||||
|
||||
@model_cli.command()
|
||||
@click.argument("model_tag", type=click.STRING)
|
||||
@click.option(
|
||||
"-f",
|
||||
"--force",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Forced push to remote model store even if it exists in remote",
|
||||
)
|
||||
@click.option(
|
||||
"-t",
|
||||
"--threads",
|
||||
default=10,
|
||||
help="Number of threads to use for upload",
|
||||
)
|
||||
@click.pass_obj
|
||||
def push(shared_options: SharedOptions, model_tag: str, force: bool, threads: int): # type: ignore (not accessed)
|
||||
"""Push Model to a remote model store."""
|
||||
model_obj = model_store.get(model_tag)
|
||||
if not model_obj:
|
||||
raise click.ClickException(f"Model {model_tag} not found in local store")
|
||||
cloud_client.push_model(
|
||||
model_obj,
|
||||
force=force,
|
||||
threads=threads,
|
||||
context=shared_options.cloud_context,
|
||||
if output == "path":
|
||||
console.print(model.path)
|
||||
elif output == "json":
|
||||
info = json.dumps(model.info.to_dict(), indent=2, default=str)
|
||||
console.print_json(info)
|
||||
else:
|
||||
console.print(
|
||||
Syntax(str(model.info.dump()), "yaml", background_color="default")
|
||||
)
|
||||
|
||||
|
||||
@model_command.command(name="list")
|
||||
@click.argument("model_name", type=click.STRING, required=False)
|
||||
@click.option(
|
||||
"-o",
|
||||
"--output",
|
||||
type=click.Choice(["json", "yaml", "table"]),
|
||||
default="table",
|
||||
)
|
||||
@inject
|
||||
def list_models(
|
||||
model_name: str,
|
||||
output: str,
|
||||
model_store: ModelStore = Provide[BentoMLContainer.model_store],
|
||||
) -> None: # type: ignore (not accessed)
|
||||
"""List Models in local store
|
||||
|
||||
\b
|
||||
# show all models saved
|
||||
$ bentoml models list
|
||||
|
||||
\b
|
||||
# show all versions of bento with the name FraudDetector
|
||||
$ bentoml models list FraudDetector
|
||||
"""
|
||||
|
||||
models = model_store.list(model_name)
|
||||
res = [
|
||||
{
|
||||
"tag": str(model.tag),
|
||||
"module": model.info.module,
|
||||
"size": human_readable_size(calc_dir_size(model.path)),
|
||||
"creation_time": model.info.creation_time.astimezone().strftime(
|
||||
"%Y-%m-%d %H:%M:%S"
|
||||
),
|
||||
}
|
||||
for model in sorted(models, key=lambda x: x.info.creation_time, reverse=True)
|
||||
]
|
||||
if output == "json":
|
||||
info = json.dumps(res, indent=2)
|
||||
console.print_json(info)
|
||||
elif output == "yaml":
|
||||
info = yaml.safe_dump(res, indent=2)
|
||||
console.print(Syntax(info, "yaml"))
|
||||
else:
|
||||
table = Table(box=None)
|
||||
table.add_column("Tag")
|
||||
table.add_column("Module")
|
||||
table.add_column("Size")
|
||||
table.add_column("Creation Time")
|
||||
for model in res:
|
||||
table.add_row(
|
||||
model["tag"],
|
||||
model["module"],
|
||||
model["size"],
|
||||
model["creation_time"],
|
||||
)
|
||||
console.print(table)
|
||||
|
||||
|
||||
@model_command.command()
|
||||
@click.argument(
|
||||
"delete_targets",
|
||||
nargs=-1,
|
||||
callback=parse_delete_targets_argument_callback,
|
||||
required=True,
|
||||
)
|
||||
@click.option(
|
||||
"-y",
|
||||
"--yes",
|
||||
"--assume-yes",
|
||||
is_flag=True,
|
||||
help="Skip confirmation when deleting a specific model",
|
||||
)
|
||||
@inject
|
||||
def delete(
|
||||
delete_targets: str,
|
||||
yes: bool,
|
||||
model_store: ModelStore = Provide[BentoMLContainer.model_store],
|
||||
bento_store: BentoStore = Provide[BentoMLContainer.bento_store],
|
||||
) -> None: # type: ignore (not accessed)
|
||||
"""Delete Model in local model store.
|
||||
|
||||
\b
|
||||
Examples:
|
||||
* Delete single model by "name:version", e.g: `bentoml models delete iris_clf:v1`
|
||||
* Bulk delete all models with a specific name, e.g.: `bentoml models delete iris_clf`
|
||||
* Bulk delete multiple models by name and version, separated by ",", e.g.: `bentoml models delete iris_clf:v1,iris_clf:v2`
|
||||
* Bulk delete multiple models by name and version, separated by " ", e.g.: `bentoml models delete iris_clf:v1 iris_clf:v2`
|
||||
* Bulk delete without confirmation, e.g.: `bentoml models delete IrisClassifier --yes`
|
||||
""" # noqa
|
||||
from bentoml.exceptions import BentoMLException
|
||||
|
||||
def check_model_is_used(tag: Tag) -> None:
|
||||
in_use: list[Tag] = []
|
||||
for bento in bento_store.list():
|
||||
if bento._model_store is not None:
|
||||
continue
|
||||
if any(model.tag == tag for model in bento.info.all_models):
|
||||
in_use.append(bento.tag)
|
||||
if in_use:
|
||||
raise BentoMLException(
|
||||
f"Model {tag} is being used by the following bentos and can't be deleted:\n "
|
||||
+ "\n ".join(map(str, in_use))
|
||||
)
|
||||
|
||||
def delete_target(target: str) -> None:
|
||||
tag = Tag.from_str(target)
|
||||
|
||||
if tag.version is None:
|
||||
to_delete_models = model_store.list(target)
|
||||
else:
|
||||
to_delete_models = [model_store.get(tag)]
|
||||
|
||||
for model in to_delete_models:
|
||||
if yes:
|
||||
delete_confirmed = True
|
||||
else:
|
||||
delete_confirmed = click.confirm(f"delete model {model.tag}?")
|
||||
|
||||
if delete_confirmed:
|
||||
check_model_is_used(model.tag)
|
||||
model_store.delete(model.tag)
|
||||
click.echo(f"{model} deleted.")
|
||||
|
||||
for target in delete_targets:
|
||||
delete_target(target)
|
||||
|
||||
|
||||
@model_command.command()
|
||||
@click.argument("model_tag", type=click.STRING)
|
||||
@click.argument("out_path", type=click.STRING, default="", required=False)
|
||||
@inject
|
||||
def export(
|
||||
model_tag: str,
|
||||
out_path: str,
|
||||
model_store: ModelStore = Provide[BentoMLContainer.model_store],
|
||||
) -> None: # type: ignore (not accessed)
|
||||
"""Export a Model to an external archive file
|
||||
|
||||
arguments:
|
||||
|
||||
\b
|
||||
MODEL_TAG: model identifier
|
||||
OUT_PATH: output path of exported model.
|
||||
If this argument is not provided, model is exported to name-version.bentomodel in the current directory.
|
||||
Besides native .bentomodel format, we also support formats like tar('tar'), tar.gz ('gz'), tar.xz ('xz'), tar.bz2 ('bz2'), and zip.
|
||||
|
||||
examples:
|
||||
|
||||
\b
|
||||
bentoml models export FraudDetector:latest
|
||||
bentoml models export FraudDetector:latest ./my_model.bentomodel
|
||||
bentoml models export FraudDetector:20210709_DE14C9 ./my_model.bentomodel
|
||||
bentoml models export FraudDetector:20210709_DE14C9 s3://mybucket/models/my_model.bentomodel
|
||||
"""
|
||||
bentomodel = model_store.get(model_tag)
|
||||
out_path = bentomodel.export(out_path)
|
||||
click.echo(f"{bentomodel} exported to {out_path}.")
|
||||
|
||||
|
||||
@model_command.command(name="import")
|
||||
@click.argument("model_path", type=click.STRING)
|
||||
def import_from(model_path: str) -> None: # type: ignore (not accessed)
|
||||
"""Import a previously exported Model archive file
|
||||
|
||||
bentoml models import ./my_model.bentomodel
|
||||
bentoml models import s3://mybucket/models/my_model.bentomodel
|
||||
"""
|
||||
bentomodel = import_model(model_path)
|
||||
click.echo(f"{bentomodel} imported.")
|
||||
|
||||
|
||||
@model_command.command()
|
||||
@click.argument("model_tag", type=click.STRING, required=False)
|
||||
@click.option(
|
||||
"-f",
|
||||
"--force",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Force pull from remote model store to local and overwrite even if it already exists in local",
|
||||
)
|
||||
@click.option(
|
||||
"-F",
|
||||
"--bentofile",
|
||||
type=click.STRING,
|
||||
default=DEFAULT_BENTO_BUILD_FILE,
|
||||
help="Path to bentofile. Default to 'bentofile.yaml'",
|
||||
)
|
||||
@click.pass_context
|
||||
@inject
|
||||
def pull(
|
||||
ctx: click.Context,
|
||||
model_tag: str | None,
|
||||
force: bool,
|
||||
bentofile: str,
|
||||
cloud_client: BentoCloudClient = Provide[BentoMLContainer.bentocloud_client],
|
||||
):
|
||||
"""Pull Model from a remote model store. If model_tag is not provided,
|
||||
it will pull models defined in bentofile.yaml.
|
||||
"""
|
||||
from click.core import ParameterSource
|
||||
|
||||
if model_tag is not None:
|
||||
if ctx.get_parameter_source("bentofile") != ParameterSource.DEFAULT:
|
||||
click.echo("-f bentofile is ignored when model_tag is provided")
|
||||
cloud_client.pull_model(model_tag, force=force, context=ctx.obj.cloud_context)
|
||||
return
|
||||
|
||||
try:
|
||||
bentofile = resolve_user_filepath(bentofile, None)
|
||||
except FileNotFoundError:
|
||||
raise InvalidArgument(f'bentofile "{bentofile}" not found')
|
||||
|
||||
with open(bentofile, "r", encoding="utf-8") as f:
|
||||
build_config = BentoBuildConfig.from_yaml(f)
|
||||
|
||||
if not build_config.models:
|
||||
raise InvalidArgument(
|
||||
"No model to pull, please provide a model tag or define models in bentofile.yaml"
|
||||
)
|
||||
for model_spec in build_config.models:
|
||||
cloud_client.pull_model(
|
||||
model_spec.tag,
|
||||
force=force,
|
||||
context=t.cast("SharedOptions", ctx.obj).cloud_context,
|
||||
query=model_spec.filter,
|
||||
)
|
||||
|
||||
|
||||
@model_command.command()
|
||||
@click.argument("model_tag", type=click.STRING)
|
||||
@click.option(
|
||||
"-f",
|
||||
"--force",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
help="Forced push to remote model store even if it exists in remote",
|
||||
)
|
||||
@click.option(
|
||||
"-t",
|
||||
"--threads",
|
||||
default=10,
|
||||
help="Number of threads to use for upload",
|
||||
)
|
||||
@click.pass_obj
|
||||
@inject
|
||||
def push(
|
||||
shared_options: SharedOptions,
|
||||
model_tag: str,
|
||||
force: bool,
|
||||
threads: int,
|
||||
model_store: ModelStore = Provide[BentoMLContainer.model_store],
|
||||
cloud_client: BentoCloudClient = Provide[BentoMLContainer.bentocloud_client],
|
||||
): # type: ignore (not accessed)
|
||||
"""Push Model to a remote model store."""
|
||||
model_obj = model_store.get(model_tag)
|
||||
if not model_obj:
|
||||
raise click.ClickException(f"Model {model_tag} not found in local store")
|
||||
cloud_client.push_model(
|
||||
model_obj,
|
||||
force=force,
|
||||
threads=threads,
|
||||
context=shared_options.cloud_context,
|
||||
)
|
||||
|
||||
@@ -46,13 +46,19 @@ def deprecated_option(*param_decls: str, **attrs: t.Any):
|
||||
return decorator
|
||||
|
||||
|
||||
def add_serve_command(cli: click.Group) -> None:
|
||||
def build_serve_command() -> click.Group:
|
||||
from bentoml._internal.configuration.containers import BentoMLContainer
|
||||
from bentoml._internal.log import configure_server_logging
|
||||
from bentoml.grpc.utils import LATEST_PROTOCOL_VERSION
|
||||
from bentoml_cli.env_manager import env_manager
|
||||
from bentoml_cli.utils import AliasCommand
|
||||
from bentoml_cli.utils import BentoMLCommandGroup
|
||||
|
||||
@cli.command(aliases=["serve-http"])
|
||||
@click.group(name="serve", cls=BentoMLCommandGroup)
|
||||
def cli():
|
||||
pass
|
||||
|
||||
@cli.command(aliases=["serve-http"], cls=AliasCommand)
|
||||
@click.argument("bento", type=click.STRING, default=".")
|
||||
@click.option(
|
||||
"--development",
|
||||
@@ -526,3 +532,8 @@ def add_serve_command(cli: click.Group) -> None:
|
||||
reload=reload,
|
||||
development_mode=False,
|
||||
)
|
||||
|
||||
return cli
|
||||
|
||||
|
||||
serve_command = build_serve_command()
|
||||
|
||||
@@ -11,10 +11,15 @@ import click
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def add_start_command(cli: click.Group) -> None:
|
||||
def build_start_command() -> click.Group:
|
||||
from bentoml._internal.configuration.containers import BentoMLContainer
|
||||
from bentoml._internal.utils import add_experimental_docstring
|
||||
from bentoml.grpc.utils import LATEST_PROTOCOL_VERSION
|
||||
from bentoml_cli.utils import BentoMLCommandGroup
|
||||
|
||||
@click.group(name="start", cls=BentoMLCommandGroup)
|
||||
def cli():
|
||||
pass
|
||||
|
||||
@cli.command(hidden=True)
|
||||
@click.argument("bento", type=click.STRING, default=".")
|
||||
@@ -480,3 +485,8 @@ def add_start_command(cli: click.Group) -> None:
|
||||
host=host,
|
||||
backlog=backlog,
|
||||
)
|
||||
|
||||
return cli
|
||||
|
||||
|
||||
start_command = build_start_command()
|
||||
|
||||
+106
-97
@@ -17,7 +17,6 @@ from click.exceptions import UsageError
|
||||
if t.TYPE_CHECKING:
|
||||
from click import Command
|
||||
from click import Context
|
||||
from click import Group
|
||||
from click import HelpFormatter
|
||||
from click import Option
|
||||
from click import Parameter
|
||||
@@ -33,7 +32,7 @@ if t.TYPE_CHECKING:
|
||||
def __call__( # pylint: disable=no-method-argument
|
||||
*args: P.args,
|
||||
**kwargs: P.kwargs,
|
||||
) -> F[P]:
|
||||
) -> t.Any:
|
||||
...
|
||||
|
||||
WrappedCLI = t.Callable[P, ClickFunctionWrapper[P]]
|
||||
@@ -197,11 +196,48 @@ class SharedOptions:
|
||||
"""This is the click.Context object that will be used in BentoML CLI."""
|
||||
|
||||
cloud_context: str | None = attr.field(default=None)
|
||||
do_not_track: bool = attr.field(default=False)
|
||||
|
||||
def with_options(self, **attrs: t.Any) -> t.Any:
|
||||
return attr.evolve(self, **attrs)
|
||||
|
||||
|
||||
def setup_verbosity(ctx: Context, param: Parameter, value: int) -> int:
|
||||
from bentoml._internal.configuration import set_debug_mode
|
||||
from bentoml._internal.configuration import set_quiet_mode
|
||||
from bentoml._internal.log import configure_logging
|
||||
|
||||
if value == 1:
|
||||
set_debug_mode(True)
|
||||
elif value == -1:
|
||||
set_quiet_mode(True)
|
||||
|
||||
configure_logging()
|
||||
return value
|
||||
|
||||
|
||||
def setup_track(ctx: Context, param: Parameter, value: bool) -> bool:
|
||||
if value:
|
||||
obj = ctx.ensure_object(SharedOptions)
|
||||
obj.do_not_track = True
|
||||
return value
|
||||
|
||||
|
||||
def setup_cloud_client(ctx: Context, param: Parameter, value: str | None) -> str | None:
|
||||
if value:
|
||||
obj = ctx.ensure_object(SharedOptions)
|
||||
obj.cloud_context = value
|
||||
return value
|
||||
|
||||
|
||||
class AliasCommand(click.Command):
|
||||
def __init__(
|
||||
self, *args: t.Any, aliases: list[str] | None = None, **kwargs: t.Any
|
||||
) -> None:
|
||||
super(AliasCommand, self).__init__(*args, **kwargs)
|
||||
self.aliases = aliases or []
|
||||
|
||||
|
||||
class BentoMLCommandGroup(click.Group):
|
||||
"""
|
||||
Click command class customized for BentoML CLI, allow specifying a default
|
||||
@@ -226,87 +262,69 @@ class BentoMLCommandGroup(click.Group):
|
||||
NUMBER_OF_COMMON_PARAMS = 5 # NOTE: 4 shared options and a option group title
|
||||
|
||||
@staticmethod
|
||||
def bentoml_common_params(func: F[P]) -> WrappedCLI[bool, bool, str | None]:
|
||||
def bentoml_common_params(f: F[P]) -> ClickFunctionWrapper[P]:
|
||||
# NOTE: update NUMBER_OF_COMMON_PARAMS when adding option.
|
||||
from bentoml._internal.configuration import DEBUG_ENV_VAR
|
||||
from bentoml._internal.configuration import QUIET_ENV_VAR
|
||||
from bentoml._internal.configuration import set_debug_mode
|
||||
from bentoml._internal.configuration import set_quiet_mode
|
||||
from bentoml._internal.log import configure_logging
|
||||
from bentoml._internal.utils.analytics import BENTOML_DO_NOT_TRACK
|
||||
|
||||
@cog.optgroup.group("Global options")
|
||||
@cog.optgroup.option(
|
||||
f = cog.optgroup.option(
|
||||
"-q",
|
||||
"--quiet",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
"verbosity",
|
||||
flag_value=-1,
|
||||
default=0,
|
||||
expose_value=False,
|
||||
envvar=QUIET_ENV_VAR,
|
||||
help="Suppress all warnings and info logs",
|
||||
)
|
||||
@cog.optgroup.option(
|
||||
callback=setup_verbosity,
|
||||
is_eager=True,
|
||||
)(f)
|
||||
f = cog.optgroup.option(
|
||||
"--verbose",
|
||||
"--debug",
|
||||
"verbose",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
"verbosity",
|
||||
flag_value=1,
|
||||
expose_value=False,
|
||||
envvar=DEBUG_ENV_VAR,
|
||||
help="Generate debug information",
|
||||
)
|
||||
@cog.optgroup.option(
|
||||
)(f)
|
||||
f = cog.optgroup.option(
|
||||
"--do-not-track",
|
||||
is_flag=True,
|
||||
default=False,
|
||||
envvar=BENTOML_DO_NOT_TRACK,
|
||||
expose_value=False,
|
||||
help="Do not send usage info",
|
||||
)
|
||||
@cog.optgroup.option(
|
||||
callback=setup_track,
|
||||
)(f)
|
||||
f = cog.optgroup.option(
|
||||
"--context",
|
||||
"cloud_context",
|
||||
type=click.STRING,
|
||||
default=None,
|
||||
help="BentoCloud context name.",
|
||||
)
|
||||
@click.pass_context
|
||||
@functools.wraps(func)
|
||||
def wrapper(
|
||||
ctx: click.Context,
|
||||
quiet: bool,
|
||||
verbose: bool,
|
||||
cloud_context: str | None,
|
||||
*args: P.args,
|
||||
**kwargs: P.kwargs,
|
||||
) -> t.Any:
|
||||
ctx.obj = SharedOptions(cloud_context=cloud_context)
|
||||
if quiet:
|
||||
set_quiet_mode(True)
|
||||
if verbose:
|
||||
logger.warning("'--quiet' passed; ignoring '--verbose/--debug'")
|
||||
elif verbose:
|
||||
set_debug_mode(True)
|
||||
|
||||
configure_logging()
|
||||
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
expose_value=False,
|
||||
callback=setup_cloud_client,
|
||||
)(f)
|
||||
f = cog.optgroup.group("Global options")(f)
|
||||
return t.cast("ClickFunctionWrapper[P]", f)
|
||||
|
||||
@staticmethod
|
||||
def bentoml_track_usage(
|
||||
func: F[P] | WrappedCLI[bool, bool, str | None],
|
||||
cmd_group: click.Group,
|
||||
**kwargs: t.Any,
|
||||
) -> WrappedCLI[bool]:
|
||||
func: F[P], cmd_group: click.Group, name: str | None
|
||||
) -> F[P]:
|
||||
from bentoml._internal.utils.analytics import BENTOML_DO_NOT_TRACK
|
||||
from bentoml._internal.utils.analytics import CliEvent
|
||||
from bentoml._internal.utils.analytics import cli_events_map
|
||||
from bentoml._internal.utils.analytics import track
|
||||
|
||||
command_name = kwargs.get("name", func.__name__)
|
||||
command_name = name or func.__name__
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(do_not_track: bool, *args: P.args, **kwargs: P.kwargs) -> t.Any:
|
||||
if do_not_track:
|
||||
@click.pass_context
|
||||
def wrapper(ctx: Context, *args: P.args, **kwargs: P.kwargs) -> t.Any:
|
||||
options = ctx.ensure_object(SharedOptions)
|
||||
if options.do_not_track:
|
||||
os.environ[BENTOML_DO_NOT_TRACK] = str(True)
|
||||
return func(*args, **kwargs)
|
||||
|
||||
@@ -349,12 +367,12 @@ class BentoMLCommandGroup(click.Group):
|
||||
|
||||
@staticmethod
|
||||
def raise_click_exception(
|
||||
func: F[P] | WrappedCLI[bool], cmd_group: click.Group, **kwargs: t.Any
|
||||
) -> ClickFunctionWrapper[t.Any]:
|
||||
func: F[P], cmd_group: click.Group, name: str | None
|
||||
) -> F[P]:
|
||||
from bentoml._internal.configuration import get_debug_mode
|
||||
from bentoml.exceptions import BentoMLException
|
||||
|
||||
command_name = kwargs.get("name", func.__name__)
|
||||
command_name = name or func.__name__
|
||||
|
||||
@functools.wraps(func)
|
||||
def wrapper(*args: P.args, **kwargs: P.kwargs) -> t.Any:
|
||||
@@ -368,58 +386,49 @@ class BentoMLCommandGroup(click.Group):
|
||||
else:
|
||||
raise ClickException(click.style(msg, fg="red")) from err
|
||||
|
||||
return t.cast("ClickFunctionWrapper[t.Any]", wrapper)
|
||||
return wrapper
|
||||
|
||||
def __init__(self, *args: t.Any, **kwargs: t.Any) -> None:
|
||||
self.aliases = kwargs.pop("aliases", [])
|
||||
super(BentoMLCommandGroup, self).__init__(*args, **kwargs)
|
||||
# these two dictionaries will store known aliases for commands and groups
|
||||
self._commands: dict[str, list[str]] = {}
|
||||
self._aliases: dict[str, str] = {}
|
||||
|
||||
def command(self, *args: t.Any, **kwargs: t.Any) -> t.Callable[[F[P]], Command]:
|
||||
if "context_settings" not in kwargs:
|
||||
kwargs["context_settings"] = {}
|
||||
kwargs["context_settings"]["max_content_width"] = 120
|
||||
aliases = kwargs.pop("aliases", None)
|
||||
|
||||
def wrapper(func: F[P]) -> Command:
|
||||
# add common parameters to command.
|
||||
options = BentoMLCommandGroup.bentoml_common_params(func)
|
||||
# Send tracking events before command finish.
|
||||
usage = BentoMLCommandGroup.bentoml_track_usage(options, self, **kwargs)
|
||||
# If BentoMLException raise ClickException instead before exit.
|
||||
wrapped = BentoMLCommandGroup.raise_click_exception(usage, self, **kwargs)
|
||||
|
||||
# move common parameters to end of the parameters list
|
||||
wrapped.__click_params__ = (
|
||||
wrapped.__click_params__[-self.NUMBER_OF_COMMON_PARAMS :]
|
||||
+ wrapped.__click_params__[: -self.NUMBER_OF_COMMON_PARAMS]
|
||||
def add_subcommands(self, group: click.Group) -> None:
|
||||
if not isinstance(group, click.MultiCommand):
|
||||
raise TypeError(
|
||||
"BentoMLCommandGroup.add_subcommands only accepts click.MultiCommand"
|
||||
)
|
||||
cmd = super(BentoMLCommandGroup, self).command(*args, **kwargs)(wrapped)
|
||||
# add aliases to a given commands if it is specified.
|
||||
if aliases is not None:
|
||||
assert cmd.name
|
||||
self._commands[cmd.name] = aliases
|
||||
self._aliases.update({alias: cmd.name for alias in aliases})
|
||||
return cmd
|
||||
if isinstance(group, BentoMLCommandGroup):
|
||||
# Common wrappers are already applied, call the super() method
|
||||
for name, cmd in group.commands.items():
|
||||
super().add_command(cmd, name)
|
||||
self._commands.update(group._commands)
|
||||
self._aliases.update(group._aliases)
|
||||
else:
|
||||
for name, cmd in group.commands.items():
|
||||
self.add_command(cmd, name)
|
||||
|
||||
return wrapper
|
||||
|
||||
def group(self, *args: t.Any, **kwargs: t.Any) -> t.Callable[[F[P]], Group]:
|
||||
aliases = kwargs.pop("aliases", None)
|
||||
|
||||
def decorator(f: F[P]):
|
||||
# create the main group
|
||||
grp = super(BentoMLCommandGroup, self).group(*args, **kwargs)(f)
|
||||
|
||||
if aliases is not None:
|
||||
assert grp.name
|
||||
self._commands[grp.name] = aliases
|
||||
self._aliases.update({k: grp.name for k in aliases})
|
||||
|
||||
return grp
|
||||
|
||||
return decorator
|
||||
def add_command(self, cmd: Command, name: str | None = None) -> None:
|
||||
assert cmd.callback is not None
|
||||
callback = BentoMLCommandGroup.bentoml_track_usage(
|
||||
cmd.callback, self, name=cmd.name
|
||||
)
|
||||
callback = BentoMLCommandGroup.raise_click_exception(
|
||||
callback, self, name=cmd.name
|
||||
)
|
||||
callback = BentoMLCommandGroup.bentoml_common_params(callback)
|
||||
cmd.params.extend(reversed(callback.__click_params__))
|
||||
del callback.__click_params__
|
||||
cmd.callback = callback
|
||||
cmd.context_settings["max_content_width"] = 120
|
||||
aliases = getattr(cmd, "aliases", None)
|
||||
if aliases:
|
||||
assert cmd.name
|
||||
self._commands[cmd.name] = aliases
|
||||
self._aliases.update({alias: cmd.name for alias in aliases})
|
||||
return super().add_command(cmd, name)
|
||||
|
||||
def resolve_alias(self, cmd_name: str):
|
||||
return self._aliases[cmd_name] if cmd_name in self._aliases else cmd_name
|
||||
|
||||
Reference in New Issue
Block a user