"vscode:/vscode.git/clone" did not exist on "c5d71a671ed77bd52adee20cfbce783eb9776f92"
Unverified Commit 988e8826 authored by mohammedabdulwahhab's avatar mohammedabdulwahhab Committed by GitHub
Browse files

fix: dynamo build should work with link syntax (#646)

parent 08fd2897
......@@ -65,6 +65,6 @@ dynamo build [SERVICE]
**Example**
```bash
cd examples
cd examples/hello_world
dynamo build hello_world:Frontend
```
......@@ -18,25 +18,36 @@
from __future__ import annotations
import json
import logging
import os
import subprocess
import typing as t
import attr
import click
import click_option_group as cog
import rich
import yaml
from bentoml._internal.bento.bento import DEFAULT_BENTO_BUILD_FILES
from bentoml._internal.bento.build_config import BentoBuildConfig
from bentoml._internal.configuration.containers import BentoMLContainer
from bentoml._internal.utils.args import set_arguments
from bentoml._internal.utils.filesystem import resolve_user_filepath
from bentoml.exceptions import InvalidArgument
from bentoml_cli.utils import is_valid_bento_name, is_valid_bento_tag
from rich.syntax import Syntax
from rich.table import Table
from simple_di import Provide, inject
from dynamo.sdk.lib.bento import Bento
if t.TYPE_CHECKING:
from bentoml._internal.bento import BentoStore
from bentoml._internal.cloud import BentoCloudClient
from bentoml._internal.container import DefaultBuilder
from click import Context, Parameter
logger = logging.getLogger(__name__)
DYNAMO_FIGLET = """
██████╗ ██╗ ██╗███╗ ██╗ █████╗ ███╗ ███╗ ██████╗
......@@ -109,13 +120,169 @@ def parse_delete_targets_argument_callback(
return delete_targets
@inject
def import_bento(
path: str,
input_format: str | None = None,
*,
protocol: str | None = None,
user: str | None = None,
passwd: str | None = None,
params: t.Optional[t.Dict[str, str]] = None,
subpath: str | None = None,
_bento_store: "BentoStore" = Provide[BentoMLContainer.bento_store],
) -> Bento:
"""
Import a bento.
Examples:
.. code-block:: python
# imports 'my_bento' from '/path/to/folder/my_bento.bento'
bentoml.import_bento('/path/to/folder/my_bento.bento')
# imports 'my_bento' from '/path/to/folder/my_bento.tar.gz'
# currently supported formats are tar.gz ('gz'),
# tar.xz ('xz'), tar.bz2 ('bz2'), and zip
bentoml.import_bento('/path/to/folder/my_bento.tar.gz')
# treats 'my_bento.ext' as a gzipped tarfile
bentoml.import_bento('/path/to/folder/my_bento.ext', 'gz')
# imports 'my_bento', which is stored as an
# uncompressed folder, from '/path/to/folder/my_bento/'
bentoml.import_bento('/path/to/folder/my_bento', 'folder')
# imports 'my_bento' from the S3 bucket 'my_bucket',
# path 'folder/my_bento.bento'
# requires `fs-s3fs <https://pypi.org/project/fs-s3fs/>`_
bentoml.import_bento('s3://my_bucket/folder/my_bento.bento')
bentoml.import_bento('my_bucket/folder/my_bento.bento', protocol='s3')
bentoml.import_bento('my_bucket', protocol='s3',
subpath='folder/my_bento.bento')
bentoml.import_bento('my_bucket', protocol='s3',
subpath='folder/my_bento.bento',
user='<AWS access key>', passwd='<AWS secret key>',
params={'acl': 'public-read',
'cache-control': 'max-age=2592000,public'})
For a more comprehensive description of what each of the keyword arguments
(:code:`protocol`, :code:`user`, :code:`passwd`,
:code:`params`, and :code:`subpath`) mean, see the
`FS URL documentation <https://docs.pyfilesystem.org/en/latest/openers.html>`_.
Args:
tag: the tag of the bento to export
path: can be one of two things:
* a folder on the local filesystem
* an `FS URL <https://docs.pyfilesystem.org/en/latest/openers.html>`_,
for example :code:`'s3://my_bucket/folder/my_bento.bento'`
protocol: (expert) The FS protocol to use when exporting. Some example protocols
are :code:`'ftp'`, :code:`'s3'`, and :code:`'userdata'`
user: (expert) the username used for authentication if required, e.g. for FTP
passwd: (expert) the username used for authentication if required, e.g. for FTP
params: (expert) a map of parameters to be passed to the FS used for
export, e.g. :code:`{'proxy': 'myproxy.net'}` for setting a
proxy for FTP
subpath: (expert) the path inside the FS that the bento should be exported to
_bento_store: the bento store to save the bento to
Returns:
Bento: the imported bento
"""
return Bento.import_from(
path,
input_format,
protocol=protocol,
user=user,
passwd=passwd,
params=params,
subpath=subpath,
).save(_bento_store)
@inject
def build_bentofile(
bentofile: str | None = None,
*,
service: str | None = None,
name: str | None = None,
version: str | None = None,
labels: dict[str, str] | None = None,
build_ctx: str | None = None,
platform: str | None = None,
bare: bool = False,
reload: bool = False,
args: dict[str, t.Any] | None = None,
_bento_store: BentoStore = Provide[BentoMLContainer.bento_store],
) -> Bento:
"""
Build a Bento base on options specified in a bentofile.yaml file.
By default, this function will look for a `bentofile.yaml` file in current working
directory.
Args:
bentofile: The file path to build config yaml file
version: Override the default auto generated version str
build_ctx: Build context directory, when used as
bare: whether to build a bento without copying files
reload: whether to reload the service
Returns:
Bento: a Bento instance representing the materialized Bento saved in BentoStore
"""
if args is not None:
set_arguments(**args)
if bentofile:
try:
bentofile = resolve_user_filepath(bentofile, None)
except FileNotFoundError:
raise InvalidArgument(f'bentofile "{bentofile}" not found')
else:
build_config = BentoBuildConfig.from_file(bentofile)
else:
for filename in DEFAULT_BENTO_BUILD_FILES:
try:
bentofile = resolve_user_filepath(filename, build_ctx)
except FileNotFoundError:
pass
else:
build_config = BentoBuildConfig.from_file(bentofile)
break
else:
build_config = BentoBuildConfig(service=service or "")
new_attrs: dict[str, t.Any] = {}
if name is not None:
new_attrs["name"] = name
if labels:
# Ensure both dictionaries are of type dict[str, str]
existing_labels: dict[str, str] = build_config.labels or {}
new_attrs["labels"] = {**existing_labels, **labels}
if new_attrs:
build_config = attr.evolve(build_config, **new_attrs)
bento = Bento.create(
build_config=build_config,
version=version,
build_ctx=build_ctx,
platform=platform,
bare=bare,
reload=reload,
)
if not bare:
return bento.save(_bento_store)
return bento
def bento_management_commands() -> click.Group:
import bentoml
from bentoml import Tag
from bentoml._internal.configuration import get_quiet_mode
from bentoml._internal.configuration.containers import BentoMLContainer
from bentoml._internal.utils import human_readable_size
from bentoml.bentos import build_bentofile, import_bento
from bentoml_cli.utils import BentoMLCommandGroup
@click.group(cls=BentoMLCommandGroup)
......
......@@ -24,12 +24,12 @@ import psutil
def create_bentoml_cli() -> click.Command:
from bentoml._internal.configuration import BENTOML_VERSION
from bentoml._internal.context import server_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.utils import get_entry_points
from dynamo.sdk.cli.bentos import bento_command
from dynamo.sdk.cli.deployment import deployment_command
from dynamo.sdk.cli.env import env_command
......
# SPDX-FileCopyrightText: Copyright (c) 2020 Atalaya Tech. Inc
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
# #
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
# #
# http://www.apache.org/licenses/LICENSE-2.0
# #
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Modifications Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES
"""
User facing python APIs for managing local bentos and build new bentos.
"""
from __future__ import annotations
import json
import logging
import os
import typing as t
import fs
import fs.errors
import fs.mirror
import yaml
from bentoml._internal.bento.bento import BENTO_PROJECT_DIR_NAME, BENTO_README_FILENAME
from bentoml._internal.bento.bento import Bento as BaseBento
from bentoml._internal.bento.bento import (
BentoApiInfo,
BentoInfo,
BentoInfoV2,
BentoModelInfo,
BentoRunnerInfo,
BentoServiceInfo,
get_default_svc_readme,
get_service_import_str,
)
from bentoml._internal.bento.build_config import BentoBuildConfig, BentoPathSpec
from bentoml._internal.configuration.containers import BentoMLContainer
from bentoml._internal.service import Service
from bentoml._internal.service.loader import load
from bentoml._internal.tag import Tag, to_snake_case
from bentoml._internal.utils.filesystem import copy_file_to_fs_folder
from bentoml._internal.utils.uri import encode_path_for_uri
from bentoml.exceptions import BentoMLException, InvalidArgument
from fs.copy import copy_file
from fs.tempfs import TempFS
from simple_di import Provide, inject
from dynamo.sdk.lib.service import LinkedServices
logger = logging.getLogger(__name__)
class Bento(BaseBento):
"""Dynamo's Bento class that extends BentoML's Bento with additional functionality."""
@classmethod
@inject
def create(
cls,
build_config: BentoBuildConfig,
version: t.Optional[str] = None,
build_ctx: t.Optional[str] = None,
platform: t.Optional[str] = None,
bare: bool = False,
reload: bool = False,
enabled_features: list[str] = Provide[BentoMLContainer.enabled_features],
) -> Bento:
from _bentoml_sdk.images import Image, populate_image_from_build_config
from _bentoml_sdk.models import BentoModel
build_ctx = (
os.getcwd()
if build_ctx is None
else os.path.realpath(os.path.expanduser(build_ctx))
)
if not os.path.isdir(build_ctx):
raise InvalidArgument(
f"Bento build context {build_ctx} does not exist or is not a directory."
)
BentoMLContainer.model_aliases.set(build_config.model_aliases)
# This also verifies that svc can be imported correctly
svc = load(build_config.service, working_dir=build_ctx, reload=reload)
# TODO: At some point we need this to take place within the load function
LinkedServices.remove_unused_edges()
if not build_config.service:
object.__setattr__(build_config, "service", get_service_import_str(svc))
is_legacy = isinstance(svc, Service)
# Apply default build options
image: Image | None = None
disable_image = "no_image" in enabled_features or is_legacy
if isinstance(svc, Service):
# for < 1.2
bento_name = (
build_config.name if build_config.name is not None else svc.name
)
else:
# for >= 1.2
svc.inject_config()
bento_name = (
build_config.name
if build_config.name is not None
else to_snake_case(svc.name)
)
build_config.envs.extend(svc.envs)
build_config.labels.update(svc.labels)
if svc.image is not None:
image = svc.image
if not disable_image:
image = populate_image_from_build_config(image, build_config, build_ctx)
build_config = build_config.with_defaults()
tag = Tag(bento_name, version)
if version is None:
tag = tag.make_new_version()
logger.debug(
'Building BentoML service "%s" from build context "%s".', tag, build_ctx
)
bento_fs = TempFS(
identifier=f"bentoml_bento_{bento_name}",
temp_dir=BentoMLContainer.tmp_bento_store_dir.get(),
)
models: list[BentoModelInfo] = []
def append_model(model: BentoModelInfo) -> None:
if model not in models:
models.append(model)
if build_config.models:
for model_spec in build_config.models:
model = BentoModel(model_spec.tag)
append_model(model.to_info(model_spec.alias))
elif is_legacy:
# XXX: legacy way to get models from service
# Add all models required by the service
for model in svc.models:
append_model(BentoModel(model.tag).to_info())
# Add all models required by service runners
for runner in svc.runners:
for model in runner.models:
append_model(BentoModel(model.tag).to_info())
if not bare:
ctx_fs = fs.open_fs(encode_path_for_uri(build_ctx))
# create ignore specs
specs = BentoPathSpec(build_config.include, build_config.exclude, build_ctx)
# Copy all files base on include and exclude, into `src` directory
relpaths = [s for s in build_config.include if s.startswith("../")]
if len(relpaths) != 0:
raise InvalidArgument(
"Paths outside of the build context directory cannot be included; use a symlink or copy those files into the working directory manually."
)
bento_fs.makedir(BENTO_PROJECT_DIR_NAME)
target_fs = bento_fs.opendir(BENTO_PROJECT_DIR_NAME)
with target_fs.open("bentofile.yaml", "w") as bentofile_yaml:
build_config.to_yaml(bentofile_yaml)
for dir_path, _, files in ctx_fs.walk():
for f in files:
path = fs.path.combine(dir_path, f.name).lstrip("/")
if specs.includes(path):
if ctx_fs.getsize(path) > 10 * 1024 * 1024:
logger.warn("File size is larger than 10MiB: %s", path)
target_fs.makedirs(dir_path, recreate=True)
copy_file(ctx_fs, path, target_fs, path)
if image is None:
# NOTE: we need to generate both Python and Conda
# first to make sure we can generate the Dockerfile correctly.
build_config.python.write_to_bento(
bento_fs, build_ctx, platform_=platform
)
build_config.conda.write_to_bento(bento_fs, build_ctx)
build_config.docker.write_to_bento(
bento_fs, build_ctx, build_config.conda
)
# Create `readme.md` file
if (
build_config.description is not None
and build_config.description.startswith("file:")
):
file_name = build_config.description[5:].strip()
if not ctx_fs.exists(file_name):
raise InvalidArgument(f"File {file_name} does not exist.")
copy_file_to_fs_folder(
ctx_fs.getsyspath(file_name),
bento_fs,
dst_filename=BENTO_README_FILENAME,
)
elif build_config.description is None and ctx_fs.exists(
BENTO_README_FILENAME
):
copy_file_to_fs_folder(
ctx_fs.getsyspath(BENTO_README_FILENAME),
bento_fs,
dst_filename=BENTO_README_FILENAME,
)
else:
with bento_fs.open(BENTO_README_FILENAME, "w") as f:
if build_config.description is None:
f.write(get_default_svc_readme(svc, version))
else:
f.write(build_config.description)
# Create 'apis/openapi.yaml' file
bento_fs.makedir("apis")
with bento_fs.open(fs.path.combine("apis", "openapi.yaml"), "w") as f:
yaml.dump(svc.openapi_spec, f)
if not is_legacy:
with bento_fs.open(fs.path.combine("apis", "schema.json"), "w") as f:
json.dump(svc.schema(), f, indent=2)
if image is None:
bento_info = BentoInfo(
tag=tag,
service=svc, # type: ignore # attrs converters do not typecheck
entry_service=svc.name,
labels=build_config.labels,
models=models,
runners=(
[BentoRunnerInfo.from_runner(r) for r in svc.runners] # type: ignore # attrs converters do not typecheck
if is_legacy
else []
),
apis=(
[BentoApiInfo.from_inference_api(api) for api in svc.apis.values()]
if is_legacy
else []
),
services=(
[
BentoServiceInfo.from_service(s)
for s in svc.all_services().values()
]
if not is_legacy
else []
),
docker=build_config.docker,
python=build_config.python,
conda=build_config.conda,
envs=build_config.envs,
schema=svc.schema() if not is_legacy else {},
)
else:
bento_info = BentoInfoV2(
tag=tag,
service=svc, # type: ignore # attrs converters do not typecheck
entry_service=svc.name,
labels=build_config.labels,
models=models,
services=(
[
BentoServiceInfo.from_service(s)
for s in svc.all_services().values()
]
if not is_legacy
else []
),
envs=build_config.envs,
schema=svc.schema() if not is_legacy else {},
image=image.freeze(bento_fs, build_config.envs, platform),
)
res = Bento(tag, bento_fs, bento_info)
if bare:
return res
# Create bento.yaml
res.flush_info()
try:
res.validate()
except BentoMLException as e:
raise BentoMLException(f"Failed to create {res!s}: {e}") from None
return res
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment