diff --git a/MANIFEST.in b/MANIFEST.in index 2f7c9b5cf19..b60dddaf72b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,9 +1,27 @@ +# 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 *~ @@ -11,3 +29,4 @@ global-exclude *.pyc global-exclude *.pyo global-exclude .git global-exclude .ipynb_checkpoints +global-exclude __pycache__ diff --git a/bentoml/__init__.py b/bentoml/__init__.py index 860db73a52e..6566ee4ed3e 100644 --- a/bentoml/__init__.py +++ b/bentoml/__init__.py @@ -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 ( @@ -32,12 +33,13 @@ __all__ = [ "__version__", "api", + "artifacts", + "config", "env", "ver", - "artifacts", - "BentoService", "save", "load", "handlers", "metrics", + "BentoService", ] diff --git a/bentoml/archive/loader.py b/bentoml/archive/loader.py index 861b99d7626..e57ae7cf807 100644 --- a/bentoml/archive/loader.py +++ b/bentoml/archive/loader.py @@ -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 diff --git a/bentoml/archive/py_module_utils.py b/bentoml/archive/py_module_utils.py index 29ea9f9a69d..57ad3f01061 100644 --- a/bentoml/archive/py_module_utils.py +++ b/bentoml/archive/py_module_utils.py @@ -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): diff --git a/bentoml/cli/__init__.py b/bentoml/cli/__init__.py index c7e6cf67cf9..b73d8532abc 100644 --- a/bentoml/cli/__init__.py +++ b/bentoml/cli/__init__.py @@ -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"] diff --git a/bentoml/config/__init__.py b/bentoml/config/__init__.py new file mode 100644 index 00000000000..a31b18726cd --- /dev/null +++ b/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) diff --git a/bentoml/config/configparser.py b/bentoml/config/configparser.py new file mode 100644 index 00000000000..9aa2c267e2c --- /dev/null +++ b/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 "".format(str(self.as_dict())) diff --git a/bentoml/utils/config.py b/bentoml/config/default_bentoml.cfg similarity index 62% rename from bentoml/utils/config.py rename to bentoml/config/default_bentoml.cfg index 643636d2a0d..1cb91d9daeb 100644 --- a/bentoml/utils/config.py +++ b/bentoml/config/default_bentoml.cfg @@ -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] +# +# \ No newline at end of file diff --git a/bentoml/deployment/sagemaker/__init__.py b/bentoml/deployment/sagemaker/__init__.py index fc7f2d2ce00..c70124198bc 100644 --- a/bentoml/deployment/sagemaker/__init__.py +++ b/bentoml/deployment/sagemaker/__init__.py @@ -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, diff --git a/bentoml/deployment/serverless/__init__.py b/bentoml/deployment/serverless/__init__.py index 0dfded50a64..465c8181b8c 100644 --- a/bentoml/deployment/serverless/__init__.py +++ b/bentoml/deployment/serverless/__init__.py @@ -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, diff --git a/bentoml/deployment/utils.py b/bentoml/deployment/utils.py index 091a4bb8ce2..42a61a1d94b 100644 --- a/bentoml/deployment/utils.py +++ b/bentoml/deployment/utils.py @@ -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, diff --git a/bentoml/utils/exceptions.py b/bentoml/exceptions.py similarity index 91% rename from bentoml/utils/exceptions.py rename to bentoml/exceptions.py index b3247d3215a..e42c4c8c9e9 100644 --- a/bentoml/utils/exceptions.py +++ b/bentoml/exceptions.py @@ -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 diff --git a/bentoml/handlers/image_handler.py b/bentoml/handlers/image_handler.py index ad31d042f9c..229d1e490b4 100644 --- a/bentoml/handlers/image_handler.py +++ b/bentoml/handlers/image_handler.py @@ -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"] diff --git a/bentoml/server/bento_api_server.py b/bentoml/server/bento_api_server.py index 3fca2662b97..7f0def46029 100644 --- a/bentoml/server/bento_api_server.py +++ b/bentoml/server/bento_api_server.py @@ -24,9 +24,12 @@ from flask import Flask, jsonify, Response, request from prometheus_client import generate_latest, Summary +from bentoml import config from bentoml.server.prediction_logger import get_prediction_logger, log_prediction from bentoml.server.feedback_logger import get_feedback_logger, log_feedback +conf = config["apiserver"] + CONTENT_TYPE_LATEST = str("text/plain; version=0.0.4; charset=utf-8") prediction_logger = get_prediction_logger() @@ -46,22 +49,27 @@ def index_view_func(bento_service): """ The index route for bento model server, it display all avaliable routes """ - # TODO: Generate a html page for user and swagger definitions endpoints = { - "/feedback": { - "description": "Predictions feedback endpoint. Expecting feedback request " - "in JSON format and must contain a `request_id` field, which can be " - "obtained from any BentoService API response header" - }, "/healthz": { "description": "Health check endpoint. Expecting an empty response with" "status code 200 when the service is in health state" - }, - "/metrics": {"description": "Prometheus metrics endpoint"}, + } } + + if conf.getboolean("enable_metrics"): + endpoints["/metrics"] = {"description": "Prometheus metrics endpoint"} + + if conf.getboolean("enable_feedback"): + endpoints["/feedback"] = { + "description": "Predictions feedback endpoint. Expecting feedback request " + "in JSON format and must contain a `request_id` field, which can be " + "obtained from any BentoService API response header" + } + for api in bento_service.get_service_apis(): path = "/{}".format(api.name) endpoints[path] = {"description": api.doc} + return jsonify(endpoints) @@ -159,10 +167,14 @@ def setup_routes(app, bento_service): app.add_url_rule("/", "index", partial(index_view_func, bento_service)) app.add_url_rule("/healthz", "healthz", healthz_view_func) - app.add_url_rule( - "/feedback", "feedback", feedback_view_func, methods=["POST", "GET"] - ) - app.add_url_rule("/metrics", "metrics", metrics_view_func) + + if conf.getboolean("enable_metrics"): + app.add_url_rule("/metrics", "metrics", metrics_view_func) + + if conf.getboolean("enable_feedback"): + app.add_url_rule( + "/feedback", "feedback", feedback_view_func, methods=["POST", "GET"] + ) for api in bento_service.get_service_apis(): setup_bento_service_api_route(app, bento_service, api) @@ -177,7 +189,7 @@ class BentoAPIServer: request data into a Service API function """ - _DEFAULT_PORT = 5000 + _DEFAULT_PORT = conf.getint("default_port") def __init__(self, bento_service, port=_DEFAULT_PORT, app_name=None): app_name = bento_service.name if app_name is None else app_name diff --git a/bentoml/server/gunicorn_server.py b/bentoml/server/gunicorn_server.py index 65397898383..4cc2a9ecefa 100644 --- a/bentoml/server/gunicorn_server.py +++ b/bentoml/server/gunicorn_server.py @@ -18,7 +18,7 @@ import multiprocessing -import gunicorn.app.base +from gunicorn.app.base import BaseApplication from gunicorn.six import iteritems @@ -34,15 +34,13 @@ def get_gunicorn_worker_count(): return (multiprocessing.cpu_count() // 2) + 1 -class GunicornApplication( - gunicorn.app.base.BaseApplication -): # pylint: disable=abstract-method +class GunicornApplication(BaseApplication): # pylint: disable=abstract-method """ A custom Gunicorn application. Usage:: - >>> import GunicornApplication + >>> from bentoml.server.gunicorn_server import GunicornApplication >>> >>> gunicorn_app = GunicornApplication(app, 5000, 2) >>> gunicorn_app.run() diff --git a/bentoml/service.py b/bentoml/service.py index b08947c0d49..a5e526b4001 100644 --- a/bentoml/service.py +++ b/bentoml/service.py @@ -24,7 +24,7 @@ from six import add_metaclass from abc import abstractmethod, ABCMeta -from bentoml.utils.exceptions import BentoMLException +from bentoml.exceptions import BentoMLException from bentoml.service_env import BentoServiceEnv from bentoml.artifact import ArtifactCollection from bentoml.utils import isidentifier diff --git a/examples/deploy-with-sagemaker/README.md b/examples/deploy-with-sagemaker/README.md index fdbac5b6dee..cdd655e4ea7 100644 --- a/examples/deploy-with-sagemaker/README.md +++ b/examples/deploy-with-sagemaker/README.md @@ -32,7 +32,7 @@ After you invoke the command, BentoML will first generated a snapshot of this mo After you invoke deploy command, BentoML will perform serveal operations for SageMaker deployment. -First, BentoML will generate snapshot of this deployment in your local file system. The default snapshot location is `~/.bentoml/deployments`. +First, BentoML will generate snapshot of this deployment in your local file system. The default snapshot location is `~/bentoml/deployments`. It will place the snapshot in the format of `platform/model_archive_name/model_archive_version/timestamp` ![ScreenShot](./file-struc.png) @@ -85,4 +85,4 @@ Delete a SageMaker deployment is as easy as deploying it. ```bash bentoml delete-deployment /ARCHIVED_PATH --platform=aws-sagemaker --region=AWS_REGION -``` \ No newline at end of file +``` diff --git a/setup.py b/setup.py index 08858ffb5f6..c1d42117e5f 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,6 @@ # limitations under the License. import os -import sys import imp import setuptools @@ -40,17 +39,7 @@ "requests", "packaging", "docker", -] - -dev_requires = [ - "pylint==2.3.1", - "flake8", - "pytest==4.6.0", - "tox-conda==0.2.0", - "twine", - "black", - "setuptools", - "gitpython>=2.0.2", + "configparser", ] cv2 = ["opencv-python"] @@ -59,18 +48,28 @@ api_server = ["gunicorn", "prometheus_client", "Werkzeug"] optional_requires = api_server + cv2 + pytorch + tensorflow -dev_all = install_requires + dev_requires + optional_requires tests_require = [ - "pytest==4.6.0", + "pytest==4.1.0", + "pytest-cov==2.7.1", "snapshottest==0.5.0", "mock==2.0.0", - "tox==3.8.4", - "pytest-cov==2.7.1", - "coverage", + "tox==3.12.1", + "coverage>=4.4", "codecov", -] -tests_require += cv2 +] + cv2 + +dev_requires = [ + "pylint==2.3.1", + "flake8", + "tox-conda==0.2.0", + "twine", + "black", + "setuptools", + "gitpython>=2.0.2", +] + tests_require + +dev_all = install_requires + dev_requires + optional_requires extras_require = { "all": dev_all, @@ -87,7 +86,8 @@ version=__version__, author="atalaya.io", author_email="contact@atalaya.io", - description="BentoML: Package and Deploy Your Machine Learning Models", + description="An open framework for building, shipping and running machine learning " + "services", long_description=long_description, long_description_content_type="text/markdown", install_requires=install_requires, @@ -112,4 +112,5 @@ "Source Code": "https://github.com/bentoml/BentoML", "Gitter Chat Room": "https://gitter.im/bentoml/BentoML", }, + include_package_data=True, # Required for '.cfg' files under bentoml/config )