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()
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)
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 _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