Skip to content

Commit

Permalink
BentoML configuration base (bentoml#158)
Browse files Browse the repository at this point in the history
* implementation of python configparser and env var based configuration

* added apiserver config as an example

* Breaking change: changed home folder from ~/.bentoml to ~/bentoml
  • Loading branch information
parano committed Jun 8, 2019
1 parent c13555c commit f143f21
Show file tree
Hide file tree
Showing 18 changed files with 284 additions and 61 deletions.
21 changes: 20 additions & 1 deletion MANIFEST.in
@@ -1,13 +1,32 @@
# Copyright 2019 Atalaya Tech, Inc.

# 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.

include README.md
graft docs
include LICENSE

# include ".cfg" files
graft bentoml/config

# Don't include examples, tests directory
prune examples
prune tests
prune docs

# Patterns to exclude from any directory
global-exclude *~
global-exclude *.pyc
global-exclude *.pyo
global-exclude .git
global-exclude .ipynb_checkpoints
global-exclude __pycache__
6 changes: 4 additions & 2 deletions bentoml/__init__.py
Expand Up @@ -17,6 +17,7 @@
from __future__ import print_function

from bentoml import handlers
from bentoml.config import config
from bentoml.version import __version__

from bentoml.service import (
Expand All @@ -32,12 +33,13 @@
__all__ = [
"__version__",
"api",
"artifacts",
"config",
"env",
"ver",
"artifacts",
"BentoService",
"save",
"load",
"handlers",
"metrics",
"BentoService",
]
2 changes: 1 addition & 1 deletion bentoml/archive/loader.py
Expand Up @@ -21,7 +21,7 @@
import tempfile

from bentoml.utils.s3 import is_s3_url, download_from_s3
from bentoml.utils.exceptions import BentoMLException
from bentoml.exceptions import BentoMLException
from bentoml.archive.config import BentoArchiveConfig


Expand Down
2 changes: 1 addition & 1 deletion bentoml/archive/py_module_utils.py
Expand Up @@ -27,7 +27,7 @@
from six import string_types, iteritems

from bentoml.utils import Path
from bentoml.utils.exceptions import BentoMLException
from bentoml.exceptions import BentoMLException


def _get_module_src_file(module):
Expand Down
2 changes: 1 addition & 1 deletion bentoml/cli/__init__.py
Expand Up @@ -28,7 +28,7 @@
from bentoml.cli.click_utils import DefaultCommandGroup, conditional_argument
from bentoml.deployment.serverless import ServerlessDeployment
from bentoml.deployment.sagemaker import SagemakerDeployment
from bentoml.utils.exceptions import BentoMLException
from bentoml.exceptions import BentoMLException

SERVERLESS_PLATFORMS = ["aws-lambda", "aws-lambda-py2", "gcp-function"]

Expand Down
67 changes: 67 additions & 0 deletions bentoml/config/__init__.py
@@ -0,0 +1,67 @@
# Copyright 2019 Atalaya Tech, Inc.

# 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.

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import os
import logging

from bentoml.utils import Path
from bentoml.exceptions import BentoMLConfigException
from bentoml.config.configparser import BentoConfigParser

logger = logging.getLogger(__name__)


def expand_env_var(env_var):
"""Expands potentially nested env var by repeatedly applying `expandvars` and
`expanduser` until interpolation stops having any effect.
"""
if not env_var:
return env_var
while True:
interpolated = os.path.expanduser(os.path.expandvars(str(env_var)))
if interpolated == env_var:
return interpolated
else:
env_var = interpolated


BENTOML_HOME = expand_env_var(os.environ.get("BENTOML_HOME", "~/bentoml"))
try:
Path(BENTOML_HOME).mkdir(exist_ok=True)
except OSError as err:
raise BentoMLConfigException(
"Error creating bentoml home dir '{}': {}".format(BENTOML_HOME, err.strerror)
)

# Default bentoml config comes with the library bentoml/config/default_bentoml.cfg
DEFAULT_CONFIG_FILE = os.path.join(os.path.dirname(__file__), "default_bentoml.cfg")
with open(DEFAULT_CONFIG_FILE, "rb") as f:
DEFAULT_CONFIG = f.read().decode("utf-8")

if "BENTML_CONFIG" in os.environ:
# User local config file for customizing bentoml
BENTOML_CONFIG_FILE = expand_env_var(os.environ.get("BENTML_CONFIG"))
else:
BENTOML_CONFIG_FILE = os.path.join(BENTOML_HOME, "bentoml.cfg")
if not os.path.isfile(BENTOML_CONFIG_FILE):
logger.info("Creating new Bentoml config file: %s", BENTOML_CONFIG_FILE)
with open(BENTOML_CONFIG_FILE, "w") as f:
f.write(DEFAULT_CONFIG)

config = BentoConfigParser(default_config=DEFAULT_CONFIG)
config.read(BENTOML_CONFIG_FILE)
99 changes: 99 additions & 0 deletions bentoml/config/configparser.py
@@ -0,0 +1,99 @@
# Copyright 2019 Atalaya Tech, Inc.

# 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.

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

import os
import logging
from collections import OrderedDict

from configparser import ConfigParser

from bentoml.exceptions import BentoMLConfigException

logger = logging.getLogger(__name__)


class BentoConfigParser(ConfigParser):
""" BentoML configuration parser
:param default_config string - serve as default value when conf key not presented in
environment var or user local config file
"""

def __init__(self, default_config, *args, **kwargs):
ConfigParser.__init__(self, *args, **kwargs)

self.bentoml_defaults = ConfigParser(*args, **kwargs)
if default_config is not None:
self.bentoml_defaults.read_string(default_config)

@staticmethod
def _env_var_name(section, key):
return "BENTOML__{}__{}".format(section.upper(), key.upper())

def get(self, section, key, **kwargs):
""" A simple hierachical config access, priority order:
1. environment var
2. user config file
3. bentoml default config file
"""
section = str(section).lower()
key = str(key).lower()

env_var = self._env_var_name(section, key)
if env_var in os.environ:
return os.environ[env_var]

if ConfigParser.has_option(self, section, key):
return ConfigParser.get(self, section, key, **kwargs)

if self.bentoml_defaults.has_option(section, key):
return self.bentoml_defaults.get(section, key, **kwargs)
else:
raise BentoMLConfigException(
"section/key '{}/{}' not found in BentoML config".format(section, key)
)

def as_dict(self, display_source=False):
cfg = {}

def add_config(cfg, source, config):
for section in config:
cfg.setdefault(section, OrderedDict())
for k, val in config.items(section=section, raw=False):
if display_source:
cfg[section][k] = (val, source)
else:
cfg[section][k] = val

add_config(cfg, "default", self.bentoml_defaults)
add_config(cfg, "bentoml.cfg", self)

for ev in os.environ:
if ev.startswith("BENTOML__"):
_, section, key = ev.split("__")
val = os.environ[ev]
if display_source:
val = (val, "env var")
cfg.setdefault(section.lower(), OrderedDict()).update(
{key.lower(): val}
)

return cfg

def __repr__(self):
return "<BentoML config: {}>".format(str(self.as_dict()))
36 changes: 28 additions & 8 deletions bentoml/utils/config.py → bentoml/config/default_bentoml.cfg
Expand Up @@ -12,11 +12,31 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import absolute_import
from __future__ import division
from __future__ import print_function

# env var
# ~/.bentomlrc
# cli args?
# user specified global conf
# This is the default configuration for BentoML. When bentoml is imported, it looks
# for a configuration file at "$BENTOML_HOME/bentoml.cfg".

[core]

logging_level = INFO
logging_config_class =
log_format =

usage_report = true


[apiserver]
default_port = 5000
enable_metrics = true
enable_feedback = true

[cli]
#
#

[tensorflow]
#
#

[pytorch]
#
#
2 changes: 1 addition & 1 deletion bentoml/deployment/sagemaker/__init__.py
Expand Up @@ -31,7 +31,7 @@
from bentoml.deployment.base_deployment import Deployment
from bentoml.deployment.utils import generate_bentoml_deployment_snapshot_path
from bentoml.utils.whichcraft import which
from bentoml.utils.exceptions import BentoMLException
from bentoml.exceptions import BentoMLException
from bentoml.deployment.sagemaker.templates import (
DEFAULT_NGINX_CONFIG,
DEFAULT_WSGI_PY,
Expand Down
2 changes: 1 addition & 1 deletion bentoml/deployment/serverless/__init__.py
Expand Up @@ -28,7 +28,7 @@
from bentoml.utils import Path
from bentoml.utils.tempdir import TempDirectory
from bentoml.utils.whichcraft import which
from bentoml.utils.exceptions import BentoMLException
from bentoml.exceptions import BentoMLException
from bentoml.deployment.base_deployment import Deployment
from bentoml.deployment.serverless.aws_lambda_template import (
create_aws_lambda_bundle,
Expand Down
5 changes: 2 additions & 3 deletions bentoml/deployment/utils.py
Expand Up @@ -20,13 +20,12 @@

from datetime import datetime

from bentoml.utils import Path
from bentoml import config


def generate_bentoml_deployment_snapshot_path(service_name, service_version, platform):
return os.path.join(
str(Path.home()),
".bentoml",
config.BENTOML_HOME,
"deployment-snapshots",
platform,
service_name,
Expand Down
6 changes: 6 additions & 0 deletions bentoml/utils/exceptions.py → bentoml/exceptions.py
Expand Up @@ -22,3 +22,9 @@ class BentoMLException(Exception):
Base class for all BentoML's errors.
Each custom exception should be derived from this class
"""

status_code = 500


class BentoMLConfigException(BentoMLException):
pass
2 changes: 1 addition & 1 deletion bentoml/handlers/image_handler.py
Expand Up @@ -24,7 +24,7 @@
from werkzeug.utils import secure_filename
from flask import Response

from bentoml.utils.exceptions import BentoMLException
from bentoml.exceptions import BentoMLException
from bentoml.handlers.base_handlers import BentoHandler, get_output_str

ACCEPTED_CONTENT_TYPES = ["images/png", "images/jpeg", "images/jpg"]
Expand Down

0 comments on commit f143f21

Please sign in to comment.