def __init__( self, bento_service=None, kind="BentoService", bentoml_deployment_version: str = Provide[ BentoMLContainer.bento_bundle_deployment_version ], ): self.kind = kind self._yaml = YAML() self._yaml.default_flow_style = False self.config = self._yaml.load( BENTOML_CONFIG_YAML_TEMPLATE.format( kind=self.kind, bentoml_version=bentoml_deployment_version, created_at=str(datetime.utcnow()), ) ) if bento_service is not None: self.config["metadata"].update( { "service_name": bento_service.name, "service_version": bento_service.version, } ) self.config["env"] = bento_service.env.to_dict() self.config['apis'] = _get_apis_list(bento_service) self.config['artifacts'] = _get_artifacts_list(bento_service)
def __init__( self, name: str = None, channels: List[str] = None, dependencies: List[str] = None, default_env_yaml_file: str = None, override_channels: bool = False, ): self._yaml = YAML() self._yaml.default_flow_style = False if default_env_yaml_file: env_yml_file = Path(default_env_yaml_file) if not env_yml_file.is_file(): raise BentoMLException( f"Can not find conda environment config yaml file at: " f"`{default_env_yaml_file}`") self._conda_env = self._yaml.load(env_yml_file) else: self._conda_env = self._yaml.load(DEFAULT_CONDA_ENV_BASE_YAML) if name: self.set_name(name) if override_channels and channels is None: raise BentoMLException( "No `conda_channels` provided while override_channels=True") if channels: self.set_channels(channels, override_channels) if dependencies: self.add_conda_dependencies(dependencies)
def dump_to_yaml_str(yaml_dict): from bentoml.utils.ruamel_yaml import YAML yaml = YAML() string_io = StringIO() yaml.dump(yaml_dict, string_io) return string_io.getvalue()
def test_conda_env_yml_file_option(tmpdir): conda_env_yml_file = os.path.join(tmpdir, 'environment.yml') with open(conda_env_yml_file, 'wb') as f: f.write(""" name: bentoml-test-conda-env channels: - test-ch-1 - test-ch-2 dependencies: - test-dep-1 """.encode()) @bentoml.env(conda_env_yml_file=conda_env_yml_file) class ServiceWithCondaDeps(bentoml.BentoService): @bentoml.api(input=DataframeInput(), batch=True) def predict(self, df): return df service_with_string = ServiceWithCondaDeps() service_with_string.save_to_dir(str(tmpdir)) from pathlib import Path from bentoml.utils.ruamel_yaml import YAML yaml = YAML() env_yml = yaml.load(Path(os.path.join(tmpdir, 'environment.yml'))) assert 'test-ch-1' in env_yml['channels'] assert 'test-ch-2' in env_yml['channels'] assert 'test-dep-1' in env_yml['dependencies']
def parse_yaml_file_callback(ctx, param, value): # pylint: disable=unused-argument yaml = YAML() yml_content = value.read() try: return yaml.load(yml_content) except Exception: raise click.BadParameter( 'Input value is not recognizable yaml file or yaml content')
class CondaEnv(object): """A wrapper around conda environment settings file, allows adding/removing conda or pip dependencies to env configuration, and supports load/export those settings from/to yaml files. The generated file is the same format as yaml file generated from `conda env export` command. """ def __init__( self, name: str = None, channels: List[str] = None, dependencies: List[str] = None, default_env_yaml_file: str = None, override_channels: bool = False, ): self._yaml = YAML() self._yaml.default_flow_style = False if default_env_yaml_file: env_yml_file = Path(default_env_yaml_file) if not env_yml_file.is_file(): raise BentoMLException( f"Can not find conda environment config yaml file at: " f"`{default_env_yaml_file}`") self._conda_env = self._yaml.load(env_yml_file) else: self._conda_env = self._yaml.load(DEFAULT_CONDA_ENV_BASE_YAML) if name: self.set_name(name) if override_channels and channels is None: raise BentoMLException( "No `conda_channels` provided while override_channels=True") if channels: self.set_channels(channels, override_channels) if dependencies: self.add_conda_dependencies(dependencies) def set_name(self, name): self._conda_env["name"] = name def add_conda_dependencies(self, conda_dependencies: List[str]): # BentoML uses conda's channel_priority=strict option by default # Adding `dependencies` to beginning of the list to take priority over the # existing conda channels self._conda_env["dependencies"] = (conda_dependencies + self._conda_env["dependencies"]) def set_channels(self, channels: List[str], override_channels=False): if override_channels and "nodefaults" not in channels: channels.append("nodefaults") self._conda_env["channels"] = channels def write_to_yaml_file(self, filepath): with open(filepath, 'wb') as output_yaml: self._yaml.dump(self._conda_env, output_yaml)
class CondaEnv(object): """A wrapper around conda environment settings file, allows adding/removing conda or pip dependencies to env configuration, and supports load/export those settings from/to yaml files. The generated file is the same format as yaml file generated from `conda env export` command. """ def __init__( self, name: str, python_version: str, conda_channels: List[str] = None, conda_dependencies: List[str] = None, ): self._yaml = YAML() self._yaml.default_flow_style = False self._conda_env = self._yaml.load( CONDA_ENV_BASE_YAML.format(name=name, python_version=python_version)) if conda_channels: self.add_channels(conda_channels) if conda_dependencies: self.add_conda_dependencies(conda_dependencies) def set_name(self, name): self._conda_env["name"] = name def get_name(self): return self._conda_env["name"] def add_conda_dependencies(self, conda_dependencies: List[str]): # BentoML uses conda's channel_priority=strict option by default # Adding `conda_dependencies` to beginning of the list to take priority over the # existing conda channels self._conda_env["dependencies"] = (conda_dependencies + self._conda_env["dependencies"]) def add_channels(self, channels: List[str]): for channel_name in channels: if channel_name not in self._conda_env["channels"]: self._conda_env["channels"] += channels else: logger.debug(f"Conda channel {channel_name} already added") def write_to_yaml_file(self, filepath): with open(filepath, 'wb') as output_yaml: self._yaml.dump(self._conda_env, output_yaml)
def wrapped_save(*args, **kwargs): if not self.is_ready: raise FailedPrecondition( "Trying to save empty artifact. An artifact needs to be `pack` " "with model instance or `load` from saved path before saving" ) # save metadata dst = args[0] # save(self, dst) if self.metadata: yaml = YAML() yaml.dump(self.metadata, Path(self._metadata_path(dst))) original = object.__getattribute__(self, item) return original(*args, **kwargs)
def configure_logging(logging_level=None): base_log_dir = os.path.expanduser(config("logging").get("BASE_LOG_DIR")) Path(base_log_dir).mkdir(parents=True, exist_ok=True) if os.path.exists(config("logging").get("logging_config")): logging_config_path = config("logging").get("logging_config") with open(logging_config_path, "rb") as f: logging_config = YAML().load(f.read()) logging.config.dictConfig(logging_config) logging.getLogger(__name__).debug( "Loaded logging configuration from %s." % logging_config_path) else: if logging_level is None: logging_level = config("logging").get("LEVEL").upper() if "LOGGING_LEVEL" in config("logging"): # Support legacy config name e.g. BENTOML__LOGGING__LOGGING_LEVEL=debug logging_level = config("logging").get("LOGGING_LEVEL").upper() if get_debug_mode(): logging_level = logging.getLevelName(logging.DEBUG) logging_config = get_logging_config_dict(logging_level, base_log_dir) logging.config.dictConfig(logging_config) logging.getLogger(__name__).debug( "Loaded logging configuration from default configuration " + "and environment variables.")
def _get_bento_service_event_properties(bento_service, properties=None): bento_service_metadata = bento_service.get_bento_service_metadata_pb() if properties is None: properties = {} artifact_types = set() input_types = set() output_types = set() for artifact in bento_service_metadata.artifacts: artifact_types.add(artifact.artifact_type) for api in bento_service_metadata.apis: input_types.add(api.input_type) output_types.add(api.output_type) if input_types: properties["input_types"] = list(input_types) if output_types: properties["output_types"] = list(output_types) if artifact_types: properties["artifact_types"] = list(artifact_types) else: properties["artifact_types"] = ["NO_ARTIFACT"] env_dict = ProtoMessageToDict(bento_service_metadata.env) if 'conda_env' in env_dict: env_dict['conda_env'] = YAML().load(env_dict['conda_env']) properties['env'] = env_dict return properties
def __init__( self, default_config_file: str = None, override_config_file: str = None, validate_schema: bool = True, ): # Default configuraiton if default_config_file is None: default_config_file = os.path.join( os.path.dirname(__file__), "default_bentoml.yml" ) with open(default_config_file, "rb") as f: self.config = YAML().load(f.read()) if validate_schema: try: SCHEMA.validate(self.config) except SchemaError as e: raise BentoMLConfigException( "Default configuration 'default_bentoml.yml' does not" " conform to the required schema." ) from e # User override configuration if override_config_file is not None: LOGGER.info("Applying user config override from %s" % override_config_file) if not os.path.exists(override_config_file): raise BentoMLConfigException( f"Config file {override_config_file} not found" ) with open(override_config_file, "rb") as f: override_config = YAML().load(f.read()) always_merger.merge(self.config, override_config) if validate_schema: try: SCHEMA.validate(self.config) except SchemaError as e: raise BentoMLConfigException( "Configuration after user override does not conform to" " the required schema." ) from e
def __init__( self, name: str, python_version: str, conda_channels: List[str] = None, conda_dependencies: List[str] = None, ): self._yaml = YAML() self._yaml.default_flow_style = False self._conda_env = self._yaml.load( CONDA_ENV_BASE_YAML.format(name=name, python_version=python_version)) if conda_channels: self.add_channels(conda_channels) if conda_dependencies: self.add_conda_dependencies(conda_dependencies)
def test_conda_channels_n_dependencies(tmpdir): @bentoml.env(conda_channels=["bentoml-test-channel"], conda_dependencies=["bentoml-test-lib"]) class ServiceWithCondaDeps(bentoml.BentoService): @bentoml.api(input=DataframeInput(), batch=True) def predict(self, df): return df service_with_string = ServiceWithCondaDeps() service_with_string.save_to_dir(str(tmpdir)) from pathlib import Path from bentoml.utils.ruamel_yaml import YAML yaml = YAML() env_yml = yaml.load(Path(os.path.join(tmpdir, 'environment.yml'))) assert 'bentoml-test-channel' in env_yml['channels'] assert 'bentoml-test-lib' in env_yml['dependencies']
def wrapped_load(*args, **kwargs): if self.packed: logger.warning( "`load` on a 'packed' artifact may lead to unexpected behaviors" ) if self.loaded: logger.warning( "`load` an artifact multiple times may lead to unexpected " "behaviors") # load metadata if exists path = args[0] # load(self, path) meta_path = self._metadata_path(path) if os.path.isfile(meta_path): with open(meta_path) as file: yaml = YAML() yaml_content = file.read() self._metadata = yaml.load(yaml_content) ret = original(*args, **kwargs) # do not set self._loaded if `load` has failed with an exception raised self._loaded = True return ret
def __init__( self, name: str = None, channels: List[str] = None, dependencies: List[str] = None, default_env_yaml_file: str = None, override_channels: bool = False, ): self._yaml = YAML() self._yaml.default_flow_style = False self.default_env_yaml_file = default_env_yaml_file if name: self.set_name(name) if override_channels and channels is None: raise BentoMLException( "No `conda_channels` provided while override_channels=True") if channels: self.set_channels(channels, override_channels) if dependencies: self.add_conda_dependencies(dependencies)
class SavedBundleConfig(object): def __init__(self, bento_service=None, kind="BentoService"): self.kind = kind self._yaml = YAML() self._yaml.default_flow_style = False self.config = self._yaml.load( BENTOML_CONFIG_YAML_TEMPLATE.format( kind=self.kind, bentoml_version=get_bentoml_deploy_version(), created_at=str(datetime.utcnow()), )) if bento_service is not None: self.config["metadata"].update({ "service_name": bento_service.name, "service_version": bento_service.version, }) self.config["env"] = bento_service.env.to_dict() self.config['apis'] = _get_apis_list(bento_service) self.config['artifacts'] = _get_artifacts_list(bento_service) def write_to_path(self, path, filename="bentoml.yml"): return self._yaml.dump(self.config, Path(os.path.join(path, filename))) @classmethod def load(cls, filepath): conf = cls() with open(filepath, "rb") as config_file: yml_content = config_file.read() conf.config = conf._yaml.load(yml_content) ver = str(conf["version"]) py_ver = conf.config["env"]["python_version"] if ver != BENTOML_VERSION: msg = ( "Saved BentoService bundle version mismatch: loading BentoService " "bundle create with BentoML version {}, but loading from BentoML " "version {}".format(conf["version"], BENTOML_VERSION)) # If major version is different, then there could be incompatible API # changes. Raise error in this case. if ver.split(".")[0] != BENTOML_VERSION.split(".")[0]: if not BENTOML_VERSION.startswith('0+untagged'): raise BentoMLConfigException(msg) else: logger.warning(msg) else: # Otherwise just show a warning. logger.warning(msg) if py_ver != PYTHON_VERSION: logger.warning( f"Saved BentoService Python version mismatch: loading " f"BentoService bundle created with Python version {py_ver}, " f"but current environment version is {PYTHON_VERSION}.") return conf def get_bento_service_metadata_pb(self): from bentoml.yatai.proto.repository_pb2 import BentoServiceMetadata bento_service_metadata = BentoServiceMetadata() bento_service_metadata.name = self.config["metadata"]["service_name"] bento_service_metadata.version = self.config["metadata"][ "service_version"] bento_service_metadata.created_at.FromDatetime( self.config["metadata"]["created_at"]) if "env" in self.config: if "setup_sh" in self.config["env"]: bento_service_metadata.env.setup_sh = self.config["env"][ "setup_sh"] if "conda_env" in self.config["env"]: bento_service_metadata.env.conda_env = dump_to_yaml_str( self.config["env"]["conda_env"]) if "pip_packages" in self.config["env"]: for pip_package in self.config["env"]["pip_packages"]: bento_service_metadata.env.pip_packages.append(pip_package) if "python_version" in self.config["env"]: bento_service_metadata.env.python_version = self.config["env"][ "python_version"] if "docker_base_image" in self.config["env"]: bento_service_metadata.env.docker_base_image = self.config[ "env"]["docker_base_image"] if "apis" in self.config: for api_config in self.config["apis"]: if 'handler_type' in api_config: # Convert handler type to input type for saved bundle created # before version 0.8.0 input_type = api_config.get('handler_type') elif 'input_type' in api_config: input_type = api_config.get('input_type') else: input_type = "unknown" if 'output_type' in api_config: output_type = api_config.get('output_type') else: output_type = "DefaultOutput" api_metadata = BentoServiceMetadata.BentoServiceApi( name=api_config["name"], docs=api_config["docs"], input_type=input_type, output_type=output_type, ) if "handler_config" in api_config: # Supports viewing API input config info for saved bundle created # before version 0.8.0 for k, v in api_config["handler_config"].items(): if k in {'mb_max_latency', 'mb_max_batch_size'}: setattr(api_metadata, k, v) else: api_metadata.input_config[k] = v else: if 'mb_max_latency' in api_config: api_metadata.mb_max_latency = api_config[ "mb_max_latency"] else: api_metadata.mb_max_latency = DEFAULT_MAX_LATENCY if 'mb_max_batch_size' in api_config: api_metadata.mb_max_batch_size = api_config[ "mb_max_batch_size"] else: api_metadata.mb_max_batch_size = DEFAULT_MAX_BATCH_SIZE if "input_config" in api_config: for k, v in api_config["input_config"].items(): api_metadata.input_config[k] = v if "output_config" in api_config: for k, v in api_config["output_config"].items(): api_metadata.output_config[k] = v api_metadata.batch = api_config.get("batch", False) bento_service_metadata.apis.extend([api_metadata]) if "artifacts" in self.config: for artifact_config in self.config["artifacts"]: artifact_metadata = BentoServiceMetadata.BentoArtifact() if "name" in artifact_config: artifact_metadata.name = artifact_config["name"] if "artifact_type" in artifact_config: artifact_metadata.artifact_type = artifact_config[ "artifact_type"] if "metadata" in artifact_config: if isinstance(artifact_config["metadata"], dict): s = Struct() s.update(artifact_config["metadata"]) artifact_metadata.metadata.CopyFrom(s) else: logger.warning( "Tried to get non-dictionary metadata for artifact " f"{artifact_metadata.name}. Ignoring metadata...") bento_service_metadata.artifacts.extend([artifact_metadata]) return bento_service_metadata def __getitem__(self, item): return self.config[item] def __setitem__(self, key, value): self.config[key] = value def __contains__(self, item): return item in self.config
def __init__( self, default_config_file: str = None, override_config_file: str = None, validate_schema: bool = True, legacy_compatibility: bool = True, ): # Default configuraiton if default_config_file is None: default_config_file = os.path.join(os.path.dirname(__file__), "default_bentoml.yml") with open(default_config_file, "rb") as f: self.config = YAML().load(f.read()) if validate_schema: try: SCHEMA.validate(self.config) except SchemaError as e: raise BentoMLConfigException( "Default configuration 'default_bentoml.yml' does not" " conform to the required schema.") from e # Legacy configuration compatibility if legacy_compatibility: try: self.config["api_server"]["port"] = config("apiserver").getint( "default_port") self.config["api_server"]["max_request_size"] = config( "apiserver").getint("default_max_request_size") self.config["marshal_server"]["max_batch_size"] = config( "marshal_server").getint("default_max_batch_size") self.config["marshal_server"]["max_latency"] = config( "marshal_server").getint("default_max_latency") self.config["marshal_server"]["request_header_flag"] = config( "marshal_server").get("marshal_request_header_flag") self.config["yatai"]["url"] = config("yatai_service").get( "url") self.config["tracing"]["zipkin_api_url"] = config( "tracing").get("zipkin_api_url") self.config["instrument"]["namespace"] = config( "instrument").get("default_namespace") except KeyError as e: raise BentoMLConfigException( "Overriding a non-existent configuration key in compatibility mode." ) from e if validate_schema: try: SCHEMA.validate(self.config) except SchemaError as e: raise BentoMLConfigException( "Configuration after applying legacy compatibility" " does not conform to the required schema.") from e # User override configuration if override_config_file is not None and os.path.exists( override_config_file): LOGGER.info("Applying user config override from %s" % override_config_file) with open(override_config_file, "rb") as f: override_config = YAML().load(f.read()) always_merger.merge(self.config, override_config) if validate_schema: try: SCHEMA.validate(self.config) except SchemaError as e: raise BentoMLConfigException( "Configuration after user override does not conform to" " the required schema.") from e
def deployment_yaml_string_to_pb(deployment_yaml_string): yaml = YAML() deployment_yaml = yaml.load(deployment_yaml_string) return deployment_dict_to_pb(deployment_yaml)
def __init__( self, default_config_file: str = None, override_config_file: str = None, validate_schema: bool = True, legacy_compatibility: bool = True, ): # Default configuraiton if default_config_file is None: default_config_file = os.path.join(os.path.dirname(__file__), "default_bentoml.yml") with open(default_config_file, "rb") as f: self.config = YAML().load(f.read()) if validate_schema: try: SCHEMA.validate(self.config) except SchemaError as e: raise BentoMLConfigException( "Default configuration 'default_bentoml.yml' does not" " conform to the required schema.") from e # Legacy configuration compatibility if legacy_compatibility: try: self.config["bento_server"]["port"] = config( "apiserver").getint("default_port") self.config["bento_server"]["workers"] = config( "apiserver").getint("default_gunicorn_workers_count") self.config["bento_server"]["max_request_size"] = config( "apiserver").getint("default_max_request_size") if "default_max_batch_size" in config("marshal_server"): self.config["bento_server"]["microbatch"][ "max_batch_size"] = config("marshal_server").getint( "default_max_batch_size") if "default_max_latency" in config("marshal_server"): self.config["bento_server"]["microbatch"][ "max_latency"] = config("marshal_server").getint( "default_max_latency") self.config["bento_server"]["metrics"]["namespace"] = config( "instrument").get("default_namespace") self.config["adapters"]["image_input"][ "default_extensions"] = [ extension.strip() for extension in config("apiserver").get( "default_image_input_accept_file_extensions"). split(",") ] except KeyError as e: raise BentoMLConfigException( "Overriding a non-existent configuration key in compatibility mode." ) from e if validate_schema: try: SCHEMA.validate(self.config) except SchemaError as e: raise BentoMLConfigException( "Configuration after applying legacy compatibility" " does not conform to the required schema.") from e # User override configuration if override_config_file is not None: LOGGER.info("Applying user config override from %s" % override_config_file) if not os.path.exists(override_config_file): raise BentoMLConfigException( f"Config file {override_config_file} not found") with open(override_config_file, "rb") as f: override_config = YAML().load(f.read()) always_merger.merge(self.config, override_config) if validate_schema: try: SCHEMA.validate(self.config) except SchemaError as e: raise BentoMLConfigException( "Configuration after user override does not conform to" " the required schema.") from e
def _create_aws_lambda_cloudformation_template_file( project_dir, namespace, deployment_name, deployment_path_prefix, api_names, bento_service_name, s3_bucket_name, py_runtime, memory_size, timeout, ): template_file_path = os.path.join(project_dir, 'template.yaml') yaml = YAML() sam_config = { 'AWSTemplateFormatVersion': '2010-09-09', 'Transform': 'AWS::Serverless-2016-10-31', 'Globals': { 'Function': { 'Timeout': timeout, 'Runtime': py_runtime }, 'Api': { 'BinaryMediaTypes': ['image~1*'], 'Cors': "'*'", 'Auth': { 'ApiKeyRequired': False, 'DefaultAuthorizer': 'NONE', 'AddDefaultAuthorizerToCorsPreflight': False, }, }, }, 'Resources': {}, 'Outputs': { 'S3Bucket': { 'Value': s3_bucket_name, 'Description': 'S3 Bucket for saving artifacts and lambda bundle', } }, } for api_name in api_names: sam_config['Resources'][api_name] = { 'Type': 'AWS::Serverless::Function', 'Properties': { 'Runtime': py_runtime, 'CodeUri': deployment_name + '/', 'Handler': 'app.{}'.format(api_name), 'FunctionName': f'{namespace}-{deployment_name}-{api_name}', 'Timeout': timeout, 'MemorySize': memory_size, 'Events': { 'Api': { 'Type': 'Api', 'Properties': { 'Path': '/{}'.format(api_name), 'Method': 'post', }, } }, 'Policies': [{ 'S3ReadPolicy': { 'BucketName': s3_bucket_name } }], 'Environment': { 'Variables': { 'BENTOML_BENTO_SERVICE_NAME': bento_service_name, 'BENTOML_API_NAME': api_name, 'BENTOML_S3_BUCKET': s3_bucket_name, 'BENTOML_DEPLOYMENT_PATH_PREFIX': deployment_path_prefix, } }, }, } yaml.dump(sam_config, Path(template_file_path)) # We add Outputs section separately, because the value should not # have "'" around !Sub with open(template_file_path, 'a') as f: f.write("""\ EndpointUrl: Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.\ amazonaws.com/Prod" Description: URL for endpoint """) return template_file_path