Repository: bentoml/BentoML
Pull request #26 (2019-04-09 09:18:21)

Implement API change based on initial feedbacks
What changes were proposed in this pull request?
------------------------------------------------
* Added `@bentoml.artifacts` and `@bentoml.env` decorator for defining artifacts and env for BentoService
* Removed BentoModel and SingleModelBentoService class - just BentoService now
* Refactored Artifact into ArtifactSpec and ArtifactInstance
* As this is a breaking change, most tests are failing now - will fix those in follow up PR

Does this close any currently open issues?
------------------------------------------
n/a

How was this patch tested?
--------------------------
n/a



Patch:
diff --git a/bentoml/__init__.py b/bentoml/__init__.py
index aa83c3f7..210ba6b1 100644
--- a/bentoml/__init__.py
+++ b/bentoml/__init__.py
@@ -19,12 +19,15 @@ from __future__ import division
 from __future__ import print_function
 
 from bentoml import handlers
+from bentoml.version import __version__
+
+from bentoml.service import BentoService, env, artifacts
 from bentoml.server import metrics
-from bentoml.model import BentoModel
-from bentoml.service import BentoService
 from bentoml.loader import load
-from bentoml.version import __version__
+from bentoml.archive import save
 
 api = BentoService.api
 
-__all__ = ['__version__', 'api', 'BentoModel', 'BentoService', 'load', 'handlers', 'metrics']
+__all__ = [
+    '__version__', 'api', 'env', 'artifacts', 'BentoService', 'save', 'load', 'handlers', 'metrics'
+]
diff --git a/bentoml/archive.py b/bentoml/archive.py
new file mode 100644
index 00000000..7d0ac38e
--- /dev/null
+++ b/bentoml/archive.py
@@ -0,0 +1,295 @@
+# BentoML - Machine Learning Toolkit for packaging and deploying models
+# Copyright (C) 2019 Atalaya Tech, Inc.
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+import re
+import os
+import tempfile
+import uuid
+from datetime import datetime
+
+from bentoml.utils import Path
+from bentoml.utils.module_helper import copy_used_py_modules
+from bentoml.utils.exceptions import BentoMLException
+from bentoml.utils.s3 import is_s3_url, upload_to_s3, download_from_s3
+from bentoml.loader import load_bentoml_config
+from bentoml.version import __version__ as BENTOML_VERSION
+
+BENTO_MODEL_SETUP_PY_TEMPLATE = """\
+import os
+import pip
+import logging
+import pkg_resources
+import setuptools
+
+def _parse_requirements(file_path):
+    pip_ver = pkg_resources.get_distribution('pip').version
+    pip_version = list(map(int, pip_ver.split('.')[:2]))
+    if pip_version >= [6, 0]:
+        raw = pip.req.parse_requirements(file_path,
+                                         session=pip.download.PipSession())
+    else:
+        raw = pip.req.parse_requirements(file_path)
+    return [str(i.req) for i in raw]
+
+try:
+    install_reqs = _parse_requirements("requirements.txt")
+except Exception:
+    logging.warning('Fail load requirements file, so using default ones.')
+    install_reqs = []
+
+setuptools.setup(
+    name='{name}',
+    version='{pypi_package_version}',
+    description="BentoML generated model module",
+    long_description=\"\"\"{long_description}\"\"\",
+    long_description_content_type="text/markdown",
+    url="https://github.com/atalaya-io/BentoML",
+    packages=setuptools.find_packages(),
+    install_requires=install_reqs,
+    include_package_data=True,
+    package_data={{
+        '{name}': ['bentoml.yml', 'artifacts/*']
+    }},
+    # TODO: add console entry_point for using model from cli
+    # entry_points={{
+    #
+    # }}
+)
+"""
+
+MANIFEST_IN_TEMPLATE = """\
+include {service_name}/bentoml.yml
+graft {service_name}/artifacts
+"""
+
+# TODO: add setup.sh and run
+BENTO_SERVER_SINGLE_MODEL_DOCKERFILE_TEMPLATE = """\
+FROM continuumio/miniconda3
+
+ENTRYPOINT [ "/bin/bash", "-c" ]
+
+EXPOSE 5000
+
+RUN apt-get update && apt-get install -y \
+      libpq-dev \
+      build-essential \
+      && rm -rf /var/lib/apt/lists/*
+
+# copy over model files
+COPY . /bento
+WORKDIR /bento
+
+ARG conda_env={conda_env_name}
+
+# update conda and setup environment
+RUN conda update conda -y
+
+# create conda env
+RUN conda env create -f /bento/environment.yml
+
+ENV PATH /opt/conda/envs/$conda_env/bin:$PATH
+
+RUN conda install pip && pip install -r /bento/requirements.txt
+
+# Run bento server with path to bento archive
+CMD ["bentoml serve --model-path=/bento"]
+"""
+
+# TODO: improve this with import hooks PEP302?
+INIT_PY_TEMPLATE = """\
+import os
+import sys
+
+__VERSION__ = "{pypi_package_version}"
+
+__module_path = os.path.abspath(os.path.split(__file__)[0])
+
+# Prepend __module_path to sys.path, to avoid name conflicts with other installed modules
+sys.path.insert(0, __module_path)
+import {module_name}
+sys.path.remove(__module_path)
+
+# Set _bento_module_path, which tells the model where to load its artifacts
+{service_name} = {module_name}.{service_name}
+{service_name}._bento_module_path = __module_path
+
+__all__ = ['__version__', '{service_name}']
+"""
+
+BENTOML_CONFIG_YAML_TEMPLATE = """\
+service_name: {service_name}
+service_version: {service_version}
+module_name: {module_name}
+module_file: {module_file}
+created_at: {created_at}
+bentoml_version: {bentoml_version}
+"""
+
+DEFAULT_BENTO_ARCHIVE_DESCRIPTION = """\
+# BentoML(bentoml.ai) generated model archive
+"""
+
+
+def _validate_version_str(version_str):
+    """
+    Validate that version str starts with char, has length >= 4
+    """
+    regex = r"^[a-zA-Z][a-zA-Z0-9_]{3,}"
+    if re.match(regex, version_str) is None:
+        raise ValueError('Invalid version format: "{}"'.format(version_str))
+
+
+def _generate_new_version_str():
+    """
+    Generate a version string in the format of YYYY_MM_DD_RandomHash
+    """
+    time_obj = datetime.now()
+    date_string = time_obj.strftime('%Y_%m_%d')
+    random_hash = uuid.uuid4().hex[:8]
+
+    return 'b' + date_string + '_' + random_hash
+
+
+# TODO: make pypi version consistent with archive version, and use Semantic versioning for both
+def save(bento_service, dst, version=None, pypi_package_version="1.0.0"):
+    """
+    Save given BentoService along with all artifacts to target path
+    """
+
+    if version is None:
+        version = _generate_new_version_str()
+    _validate_version_str(version)
+
+    s3_url = None
+    if is_s3_url(dst):
+        s3_url = os.path.join(dst, bento_service.name, version)
+        # TODO: check s3_url not exist, otherwise raise exception
+        temp_dir = tempfile.mkdtemp()
+        Path(temp_dir, bento_service.name).mkdir(parents=True, exist_ok=True)
+        # Update path to subfolder in the form of 'base/service_name/version/'
+        path = os.path.join(temp_dir, bento_service.name, version)
+    else:
+        Path(os.path.join(dst), bento_service.name).mkdir(parents=True, exist_ok=True)
+        # Update path to subfolder in the form of 'base/service_name/version/'
+        path = os.path.join(dst, bento_service.name, version)
+
+        if os.path.exists(path):
+            raise ValueError("Version {version} in Path: {dst} already "
+                             "exist.".format(version=version, dst=dst))
+
+    os.mkdir(path)
+    module_base_path = os.path.join(path, bento_service.name)
+    os.mkdir(module_base_path)
+
+    # write README.md with user model's docstring
+    if bento_service.__class__.__doc__:
+        model_description = bento_service.__class__.__doc__.strip()
+    else:
+        model_description = DEFAULT_BENTO_ARCHIVE_DESCRIPTION
+    with open(os.path.join(path, 'README.md'), 'w') as f:
+        f.write(model_description)
+
+    # save all model artifacts to 'base_path/name/artifacts/' directory
+    bento_service.artifacts.save(module_base_path)
+
+    # write conda environment, requirement.txt
+    bento_service.env.save(path)
+
+    # TODO: add bentoml.find_packages helper for more fine grained control over
+    # this process, e.g. packages=find_packages(base, [], exclude=[], used_module_only=True)
+    # copy over all custom model code
+    module_name, module_file = copy_used_py_modules(bento_service.__class__.__module__,
+                                                    os.path.join(path, bento_service.name))
+
+    if os.path.isabs(module_file):
+        module_file = module_name.replace('.', os.sep) + '.py'
+
+    # create __init__.py
+    with open(os.path.join(path, bento_service.name, '__init__.py'), "w") as f:
+        f.write(
+            INIT_PY_TEMPLATE.format(service_name=bento_service.name, module_name=module_name,
+                                    pypi_package_version=pypi_package_version))
+
+    # write setup.py, make exported model pip installable
+    setup_py_content = BENTO_MODEL_SETUP_PY_TEMPLATE.format(
+        name=bento_service.name, pypi_package_version=pypi_package_version,
+        long_description=model_description)
+    with open(os.path.join(path, 'setup.py'), 'w') as f:
+        f.write(setup_py_content)
+
+    with open(os.path.join(path, 'MANIFEST.in'), 'w') as f:
+        f.write(MANIFEST_IN_TEMPLATE.format(service_name=bento_service.name))
+
+    # write Dockerfile
+    with open(os.path.join(path, 'Dockerfile'), 'w') as f:
+        f.write(
+            BENTO_SERVER_SINGLE_MODEL_DOCKERFILE_TEMPLATE.format(
+                conda_env_name=bento_service.env.get_conda_env_name()))
+
+    # write bentoml.yml
+    bentoml_yml_content = BENTOML_CONFIG_YAML_TEMPLATE.format(
+        service_name=bento_service.name, bentoml_version=BENTOML_VERSION, service_version=version,
+        module_name=module_name, module_file=module_file, created_at=str(datetime.now()))
+    with open(os.path.join(path, 'bentoml.yml'), 'w') as f:
+        f.write(bentoml_yml_content)
+    # Also write bentoml.yml to module base path to make it accessible
+    # as package data after pip installed as a python package
+    with open(os.path.join(module_base_path, 'bentoml.yml'), 'w') as f:
+        f.write(bentoml_yml_content)
+
+    if s3_url:
+        upload_to_s3(s3_url, path)
+        return s3_url
+    else:
+        return path
+
+
+# TODO: consolidate this with loader module
+def load(bento_service_cls, path=None):
+    # TODO: add model.env.verify() to check dependencies and python version etc
+
+    if bento_service_cls._bento_module_path is not None:
+        # When calling load from pip installled bento model, use installed
+        # python package for loading and the same path for '/artifacts'
+
+        # TODO: warn user that 'path' parameter is ignored if it's not None here
+        path = bento_service_cls._bento_module_path
+        artifacts_path = path
+    else:
+        if path is None:
+            raise BentoMLException("Loading path is required for BentoArchive: {}.".format(
+                bento_service_cls.name()))
+
+        # When calling load on generated archive directory, look for /artifacts
+        # directory under module sub-directory
+        if is_s3_url(path):
+            temporary_path = tempfile.mkdtemp()
+            download_from_s3(path, temporary_path)
+            # Use loacl temp path for the following loading operations
+            path = temporary_path
+
+        artifacts_path = os.path.join(path, bento_service_cls.name())
+
+    bentoml_config = load_bentoml_config(path)
+
+    bento_service = bento_service_cls.load(artifacts_path)
+
+    bento_service._version = bentoml_config['service_version']
+    return bento_service
diff --git a/bentoml/artifacts/__init__.py b/bentoml/artifact/__init__.py
similarity index 63%
rename from bentoml/artifacts/__init__.py
rename to bentoml/artifact/__init__.py
index 5d271b00..e77854e0 100644
--- a/bentoml/artifacts/__init__.py
+++ b/bentoml/artifact/__init__.py
@@ -18,13 +18,13 @@ from __future__ import absolute_import
 from __future__ import division
 from __future__ import print_function
 
-from bentoml.artifacts.artifact import Artifact, ArtifactCollection
-from bentoml.artifacts.pickle_artifact import PickleArtifact
-from bentoml.artifacts.pytorch_model_artifact import PytorchModelArtifact
-from bentoml.artifacts.text_file_artifact import TextFileArtifact
-from bentoml.artifacts.tf_keras_model_artifact import TfKerasModelArtifact
+from bentoml.artifact.artifact import ArtifactSpec, ArtifactInstance, ArtifactCollection
+from bentoml.artifact.pickle_artifact import PickleArtifact
+from bentoml.artifact.pytorch_model_artifact import PytorchModelArtifact
+from bentoml.artifact.text_file_artifact import TextFileArtifact
+from bentoml.artifact.tf_keras_model_artifact import TfKerasModelArtifact
 
 __all__ = [
-    'Artifact', 'ArtifactCollection', 'PickleArtifact', 'PytorchModelArtifact', 'TextFileArtifact',
-    'TfKerasModelArtifact'
+    'ArtifactSpec', 'ArtifactInstance', 'ArtifactCollection', 'PickleArtifact',
+    'PytorchModelArtifact', 'TextFileArtifact', 'TfKerasModelArtifact'
 ]
diff --git a/bentoml/artifact/artifact.py b/bentoml/artifact/artifact.py
new file mode 100644
index 00000000..476b7f56
--- /dev/null
+++ b/bentoml/artifact/artifact.py
@@ -0,0 +1,118 @@
+# BentoML - Machine Learning Toolkit for packaging and deploying models
+# Copyright (C) 2019 Atalaya Tech, Inc.
+
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU General Public License for more details.
+
+# You should have received a copy of the GNU General Public License
+# along with this program.  If not, see <http://www.gnu.org/licenses/>.
+
+from __future__ import absolute_import
+from __future__ import division
+from __future__ import print_function
+
+import os
+
+from abc import ABCMeta, abstractmethod
+from six import add_metaclass, iteritems
+
+ARTIFACTS_SUBPATH = "artifacts"
+
+
+class ArtifactSpec(object):
+    """
+    Artifact is a spec describing how to pack and load different types
+    of model dependencies to/from file system.
+
+    A call to pack and load should return an ArtifactInstance that can
+    be saved to file system and retrieved for service workload
+    """
+
+    def __init__(self, name):
+        self._name = name
+
+    @property
+    def name(self):
+        return self._name
+
+    def pack(self, data):
+        """
+        Pack the in-memory representation of the target artifacts and make sure
+        they are ready for 'save' and 'get'
+
+        Note: add "# pylint:disable=arguments-differ" to child class's pack method
+        """
+
+    def load(self, path):
+        """
+        Load artifact assuming it was 'self.save' on the given base_path
+        """
+
+
+class ArtifactInstance(object):
+
+    def __init__(self, spec):
+        self._spec = spec
+
+    @property
+    def spec(self):
+        return self._spec
+
+    def save(self, dst):
+        """
+        Save artifact to given dst path
+        """
+
+    def get(self):
+        """
+        Get returns an python object which provides all the functionality this
+        artifact provides
+        """
+
+
+class ArtifactCollection(dict):
+    """
+    A dict of artifact instances (artifact.name -> artifact_instance)
+    """
+
+    def __setitem__(self, key, artifact):
+        if key != artifact.spec.name:
+            raise ValueError('Must use Artifact name as key, {key} not equal to {name}'.format(
+                key, artifact.name))
+
+        self.add(artifact)
+
+    def __getattr__(self, item):
+        return self[item].get()
+
+    def add(self, artifact):
+        if not isinstance(artifact, ArtifactInstance):
+            raise TypeError('ArtifactCollection only accepts type bentoml.ArtifactInstance,'
+                            'Must call Artifact#pack or Artifact#load before adding to'
+                            'an ArtifactCollection')
+
+        super(ArtifactCollection, self).__setitem__(artifact.spec.name, artifact)
+
+    def save(self, path):
+        save_path = os.path.join(path, ARTIFACTS_SUBPATH)
+        os.mkdir(save_path)
+        for artifact in self.values():
+            artifact.save(save_path)
+
+    @classmethod
+    def load(cls, path, artifacts_spec):
+        load_path = os.path.join(path, ARTIFACTS_SUBPATH)
+        artifacts = cls()
+
+        for artifact_spec in artifacts_spec:
+            artifact_instance = artifact_spec.load(load_path)
+            artifacts.add(artifact_instance)
+
+        return artifacts
\ No newline at end of file
diff --git a/bentoml/artifacts/pickle_artifact.py b/bentoml/artifact/pickle_artifact.py
similarity index 64%
rename from bentoml/artifacts/pickle_artifact.py
rename to bentoml/artifact/pickle_artifact.py
index 5d853fb5..691f0f09 100644
--- a/bentoml/artifacts/pickle_artifact.py
+++ b/bentoml/artifact/pickle_artifact.py
@@ -19,47 +19,50 @@ from __future__ import division
 from __future__ import print_function
 
 import os
-from six import string_types
+
 import dill
+from six import string_types
 
-from bentoml.artifacts import Artifact
+from bentoml.artifact import ArtifactSpec, ArtifactInstance
 
 
-class PickleArtifact(Artifact):
+class PickleArtifact(ArtifactSpec):
     """
-    Abstraction for saving/loading python objects with customizable pickle_module
+    Abstraction for saving/loading python objects with pickle serialization
     """
 
     def __init__(self, name, pickle_module=dill, pickle_extension='.pkl'):
-        self.obj = None
+        super(PickleArtifact, self).__init__(name)
+
         self._pickle_extension = pickle_extension
+
         if isinstance(pickle_module, string_types):
             self._pickle = __import__(pickle_module)
         else:
             self._pickle = pickle_module
-        super(PickleArtifact, self).__init__(name)
 
     def _pkl_file_path(self, base_path):
         return os.path.join(base_path, self.name + self._pickle_extension)
 
     def pack(self, obj):  # pylint:disable=arguments-differ
-        self.obj = obj
+        return _PickleArtifactInstance(self, obj)
+
+    def load(self, path):
+        with open(self._pkl_file_path(path), "rb") as pkl_file:
+            obj = self._pickle.load(pkl_file)
+        return self.pack(obj)
 
-    def get(self):
-        if not self.obj:
-            # TODO: create special exception type for this?
-            raise Exception("Must 'pack' artifact before 'get'.")
 
-        return self.obj
+class _PickleArtifactInstance(ArtifactInstance):
 
-    def load(self, base_path):
-        with open(self._pkl_file_path(base_path), "rb") as pkl_file:
-            self.obj = self._pickle.load(pkl_file)
+    def __init__(self, spec, obj):
+        super(_PickleArtifactInstance, self).__init__(spec)
 
-    def save(self, base_path):
-        if not self.obj:
-            raise RuntimeError("Must 'pack' artifact before 'save'.")
+        self._obj = obj
+
+    def get(self):
+        return self._obj
 
-        with open(self._pkl_file_path(base_path), "wb") as pkl_file:
-            self._pickle.dump(self.obj, pkl_file)
-        # TODO: catch error when file does not exist
+    def save(self, dst):
+        with open(self.spec._pkl_file_path(dst), "wb") as pkl_file:
+            self.spec._pickle.dump(self._obj, pkl_file)
\ No newline at end of file
diff --git a/bentoml/artifacts/pytorch_model_artifact.py b/bentoml/artifact/pytorch_model_artifact.py
similarity index 65%
rename from bentoml/artifacts/pytorch_model_artifact.py
rename to bentoml/artifact/pytorch_model_artifact.py
index d90a58b4..5bb4ed56 100644
--- a/bentoml/artifacts/pytorch_model_artifact.py
+++ b/bentoml/artifact/pytorch_model_artifact.py
@@ -21,27 +21,43 @@ from __future__ import print_function
 import os
 from six import string_types
 
-from bentoml.artifacts import Artifact
+from bentoml.artifact import ArtifactSpec, ArtifactInstance
 
 
-class PytorchModelArtifact(Artifact):
+class PytorchModelArtifact(ArtifactSpec):
     """
     Abstraction for saving/loading objects with torch.save and torch.load
     """
 
     def __init__(self, name, pickle_module='dill', file_extension='.pt'):
-        self.model = None
+        super(PytorchModelArtifact, self).__init__(name)
         self._file_extension = file_extension
         if isinstance(pickle_module, string_types):
             self._pickle = __import__(pickle_module)
         else:
             self._pickle = pickle_module
-        super(PytorchModelArtifact, self).__init__(name)
 
-    def file_path(self, base_path):
+    def _file_path(self, base_path):
         return os.path.join(base_path, self.name + self._file_extension)
 
     def pack(self, model):  # pylint:disable=arguments-differ
+        return _PytorchModelArtifactInstance(self, model)
+
+    def load(self, base_path):
+        try:
+            import torch
+        except ImportError:
+            raise ImportError("torch package is required to use PytorchModelArtifact")
+
+        model = torch.load(self._file_path(base_path), pickle_module=self._pickle)
+        return self.pack(model)
+
+
+class _PytorchModelArtifactInstance(ArtifactInstance):
+
+    def __init__(self, spec, model):
+        super(_PytorchModelArtifactInstance, self).__init__(spec)
+
         try:
             import torch
         except ImportError:
@@ -50,19 +66,16 @@ class PytorchModelArtifact(Artifact):
         if not isinstance(model, torch.nn.Module):
             raise TypeError("PytorchModelArtifact can only pack type 'torch.nn.Module'")
 
-        self.model = model
+        self._model = model
 
     def get(self):
-        return self.model
+        return self._model
 
     def save(self, base_path):
-        if not self.model:
-            raise RuntimeError("Must 'pack' artifact before 'save'.")
-
-        import torch
-        torch.save(self.model, self.file_path(base_path), pickle_module=self._pickle)
+        try:
+            import torch
+        except ImportError:
+            raise ImportError("torch package is required to use PytorchModelArtifact")
 
-    def load(self, base_path):
-        import torch
-        self.model = torch.load(self.file_path(base_path), pickle_module=self._pickle)
-        self.model.eval()
+        return torch.save(self._model, self.spec._file_path(base_path),
+                          pickle_module=self.spec._pickle)
diff --git a/bentoml/artifacts/text_file_artifact.py b/bentoml/artifact/text_file_artifact.py
similarity index 59%
rename from bentoml/artifacts/text_file_artifact.py
rename to bentoml/artifact/text_file_artifact.py
index 653eaa8b..8936db3e 100644
--- a/bentoml/artifacts/text_file_artifact.py
+++ b/bentoml/artifact/text_file_artifact.py
@@ -19,37 +19,44 @@ from __future__ import division
 from __future__ import print_function
 
 import os
-from bentoml.artifacts import Artifact
 
+from bentoml.artifact import ArtifactSpec, ArtifactInstance
+from bentoml.utils.exceptions import BentoMLException
 
-class TextFileArtifact(Artifact):
+
+class TextFileArtifact(ArtifactSpec):
     """
     Abstraction for saving/loading string to/from text files
     """
 
     def __init__(self, name, file_extension='.txt', encoding='utf8'):
-        self.content = None
-        # TODO: validate file_extension (with a regex?)
+        # TODO: validate file name and extension?
+        super(TextFileArtifact, self).__init__(name)
         self._file_extension = file_extension
         self._encoding = encoding
-        super(TextFileArtifact, self).__init__(name)
 
-    def text_file_path(self, base_path):
-        os.path.join(base_path, self.name + self._file_extension)
+    def _text_file_path(self, base_path):
+        return os.path.join(base_path, self.name + self._file_extension)
 
     def load(self, base_path):
-        with open(self.text_file_path(base_path), "rb") as f:
-            self.content = f.read().decode(self._encoding)
+        with open(self._text_file_path(base_path), "rb") as f:
+            content = f.read().decode(self._encoding)
+        return self.pack(content)
 
-    def save(self, base_path):
-        if not self.content:
-            raise RuntimeError("Must 'pack' artifact before 'save'.")
+    def pack(self, content):  # pylint:disable=arguments-differ
+        return _TextFileArtifactInstance(self, content)
 
-        with open(self.text_file_path(base_path), "wb") as f:
-            f.write(self.content.encode(self._encoding))
 
-    def pack(self, content):  # pylint:disable=arguments-differ
-        self.content = content
+class _TextFileArtifactInstance(ArtifactInstance):
+
+    def __init__(self, spec, content):
+        super(_TextFileArtifactInstance, self).__init__(spec)
+
+        self._content = content
 
     def get(self):
-        return self.content
+        return self._content
+
+    def save(self, base_path):
+        with open(self.spec._text_file_path(base_path), "wb") as f:
+            f.write(self._content.encode(self.spec._encoding))
diff --git a/bentoml/artifacts/tf_keras_model_artifact.py b/bentoml/artifact/tf_keras_model_artifact.py
similarity index 59%
rename from bentoml/artifacts/tf_keras_model_artifact.py
rename to bentoml/artifact/tf_keras_model_artifact.py
index de31d178..f609822f 100644
--- a/bentoml/artifacts/tf_keras_model_artifact.py
+++ b/bentoml/artifact/tf_keras_model_artifact.py
@@ -20,37 +20,51 @@ from __future__ import print_function
 
 import os
 
-from bentoml.artifacts import Artifact
+from bentoml.artifact import ArtifactSpec, ArtifactInstance
 
 
-class TfKerasModelArtifact(Artifact):
+class TfKerasModelArtifact(ArtifactSpec):
     """
     Abstraction for saving/loading tensorflow keras model
     """
 
     def __init__(self, name, model_extension='.h5'):
-        self.model = None
-        self._model_extension = model_extension
         super(TfKerasModelArtifact, self).__init__(name)
+        self._model_extension = model_extension
 
-    def model_file_path(self, base_path):
+    def _model_file_path(self, base_path):
         return os.path.join(base_path, self.name + self._model_extension)
 
     def pack(self, model):  # pylint:disable=arguments-differ
-        self.model = model
-
-    def get(self):
-        return self.model
+        return _TfKerasModelArtifactInstance(self, model)
 
     def load(self, base_path):
         try:
-            from tensorflow.keras.models import load_model
+            from tensorflow.python.keras.models import load_model
         except ImportError:
             raise ImportError("tensorflow package is required to use TfKerasModelArtifact")
-        self.model = load_model(self.model_file_path(base_path))
+
+        model = load_model(self._model_file_path(base_path))
+        return self.pack(model)
+
+
+class _TfKerasModelArtifactInstance(ArtifactInstance):
+
+    def __init__(self, spec, model):
+        super(_TfKerasModelArtifactInstance, self).__init__(spec)
+
+        try:
+            from tensorflow.python.keras.engine import training
+        except ImportError:
+            raise ImportError("tensorflow package is required to use TfKerasModelArtifact")
+
+        if not isinstance(model, training.Model):
+            raise ValueError('Expected `model` argument to be a `Model` instance')
+
+        self._model = model
 
     def save(self, base_path):
-        if not self.model:
-            raise RuntimeError("Must 'pack' artifact before 'save'.")
+        return self._model.save(self.spec._model_file_path(base_path))
 
-        self.model.save(self.model_file_path(base_path))
+    def get(self):
+        return self._model
diff --git a/bentoml/artifacts/artifact.py b/bentoml/artifacts/artifact.py
deleted file mode 100644
index fd67e0e8..00000000
--- a/bentoml/artifacts/artifact.py
+++ /dev/null
@@ -1,133 +0,0 @@
-# BentoML - Machine Learning Toolkit for packaging and deploying models
-# Copyright (C) 2019 Atalaya Tech, Inc.
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
-import os
-
-from abc import ABCMeta, abstractmethod
-from six import add_metaclass, iteritems
-
-ARTIFACTS_SUBPATH = "artifacts"
-
-
-@add_metaclass(ABCMeta)
-class Artifact(object):
-    """
-    Artifact is the base abstraction for saving and loading Model dependencies
-    to/from file system.
-    """
-
-    def __init__(self, name):
-        self.name = name
-
-    @abstractmethod
-    def save(self, base_path):
-        """
-        Save artifact to given base_path
-        """
-
-    @abstractmethod
-    def load(self, base_path):
-        """
-        Load artifact assuming it was 'self.save' on the given base_path
-        """
-
-    @abstractmethod
-    def pack(self, data):
-        """
-        Pack the in-memory representation of the target artifacts and make sure
-        they are ready for 'save' and 'get'
-
-        Note: add "# pylint:disable=arguments-differ" to child class's pack method
-        """
-
-    @abstractmethod
-    def get(self):
-        """
-        Get returns an python object which provides all the functionality this
-        artifact provides
-        """
-
-
-class ArtifactCollection(object):
-    """
-    A collection of artifacts
-    """
-
-    def __init__(self, collection_subpath=ARTIFACTS_SUBPATH):
-        self._artifacts_dict = dict()
-        self._collection_subpath = collection_subpath
-
-    def add(self, artifact):
-        if artifact.name in self._artifacts_dict.keys():
-            # TODO: add warn about overwrite
-            pass
-
-        if not isinstance(artifact, Artifact):
-            raise TypeError('ArtifactCollection only accepts type bentoml.Artifact')
-
-        self._artifacts_dict[artifact.name] = artifact
-
-    def save(self, path):
-        save_path = os.path.join(path, self._collection_subpath)
-        os.mkdir(save_path)
-        for artifact in self._artifacts_dict.values():
-            artifact.save(save_path)
-
-    def load(self, path):
-        load_path = os.path.join(path, self._collection_subpath)
-        for artifact in self._artifacts_dict.values():
-            artifact.load(load_path)
-
-    def __getitem__(self, item):
-        # TODO: Catch and handle invalid attribute error
-        return self._artifacts_dict.__getitem__(item).get()
-
-    def __getattr__(self, item):
-        # TODO: Catch and handle invalid attribute error
-        return self._artifacts_dict.__getitem__(item).get()
-
-    def pack(self, *args, **kwargs):
-        """
-        ArtifactCollection.pack is a wrapper that allow packing a list
-        of artifacts with one method call
-
-        >>> from bentoml.artifacts import PickleArtifact
-        >>> artifacts = ArtifactsCollection
-        >>> artifacts.add(PickleArtifact('clf'))
-        >>> artifacts.add(PickleArtifact('feature_columns'))
-        >>> # Pack each artifact individually:
-        >>> artifacts.pack('clf', my_clf)
-        >>> # or
-        >>> artifacts.pack(feature_columns=my_feature_columns)
-        >>> # or pack multiple artifacts at one time
-        >>> artifacts.pack(clf=my_clf, feature_columns=my_feature_columns)
-        """
-        if args and args[0] in self._artifacts_dict:
-            self._artifacts_dict[args[0]].pack(*args[1:], **kwargs)
-        elif not args and kwargs:
-            for artifact_name, artifact_args in iteritems(kwargs):
-                if isinstance(artifact_args, dict):
-                    self._artifacts_dict[artifact_name].pack(**artifact_args)
-                elif isinstance(artifact_args, list):
-                    self._artifacts_dict[artifact_name].pack(*artifact_args)
-                else:
-                    self._artifacts_dict[artifact_name].pack(artifact_args)
-
-                # TODO: catch error when artifact_name not in the dict
diff --git a/bentoml/handlers/utils.py b/bentoml/handlers/utils.py
index 70ae5937..4d7b3ac1 100644
--- a/bentoml/handlers/utils.py
+++ b/bentoml/handlers/utils.py
@@ -32,7 +32,7 @@ def merge_dicts(x, y):
         temp = x.copy()
         temp.update(y)
         return temp
-    
+
 
 def generate_cli_default_parser():
     """
diff --git a/bentoml/loader.py b/bentoml/loader.py
index ff022a98..c023aed9 100644
--- a/bentoml/loader.py
+++ b/bentoml/loader.py
@@ -31,39 +31,37 @@ from bentoml.utils.s3 import is_s3_url, download_from_s3
 
 class _LoadedBentoServiceWrapper(BentoService):
 
-    def __init__(self, model_service_class, path, config):
-        super(_LoadedBentoServiceWrapper, self).__init__()
+    def __init__(self, bento_service_class, path, config):
         self._path = path
         self._config = config
-        self._model_service = model_service_class()
-        self._wrap_api_funcs()
+        self._bento_service_class = bento_service_class
+        self._bento_service = None
         self.loaded = False
 
-    def load(self, path=None):
-        if path is not None:
-            # TODO: warn user path is ignored when using pip installed bentoML model
-            pass
-        self._model_service.load(self._path)
+    def load(self):
+        from bentoml import archive
+        self._bento_service = archive.load(self._bento_service_class, self._path)
+        self._wrap_api_funcs()
         self.loaded = True
 
     def get_service_apis(self):
-        return self._model_service.get_service_apis()
+        return self._bento_service.get_service_apis()
 
     @property
     def name(self):
-        return self._config['model_name']
+        return self._config['service_name']
 
     @property
     def version(self):
-        return self._config['model_version']
+        return self._config['service_version']
 
     def _wrap_api_funcs(self):
         """
-        Add target ModelService's API methods to the returned wrapper class
+        Add target BentoService's API methods to the returned wrapper class
         """
         for api in self.get_service_apis():
             setattr(self, api.name,
-                    self._model_service.__getattribute__(api.name).__get__(self._model_service))
+                    self._bento_service.__getattribute__(api.name).__get__(self._bento_service))
 
 
 def load_bentoml_config(path):
@@ -71,7 +69,7 @@ def load_bentoml_config(path):
         with open(os.path.join(path, 'bentoml.yml'), 'r') as f:
             bentml_yml_content = f.read()
     except FileNotFoundError:
-        raise ValueError("Bentoml can't locate model config file 'bentoml.yml' "
+        raise ValueError("Bentoml can't locate bentoml config file 'bentoml.yml' "
                          " in the give path: {}".format(path))
 
     yaml = YAML()
@@ -102,7 +100,7 @@ def load(path, lazy_load=False):
     config = load_bentoml_config(path)
 
     # Load target module containing BentoService class from given path
-    module_file_path = os.path.join(path, config['model_name'], config['module_file'])
+    module_file_path = os.path.join(path, config['service_name'], config['module_file'])
 
     module_name = config['module_name']
     if module_name in sys.modules:
@@ -122,7 +120,7 @@ def load(path, lazy_load=False):
         import imp
         module = imp.load_source(module_name, module_file_path)
 
-    model_service_class = module.__getattribute__(config['model_name'])
+    model_service_class = module.__getattribute__(config['service_name'])
     loaded_model = _LoadedBentoServiceWrapper(model_service_class, path, config)
 
     if not lazy_load:
diff --git a/bentoml/model.py b/bentoml/model.py
deleted file mode 100644
index 8b236671..00000000
--- a/bentoml/model.py
+++ /dev/null
@@ -1,345 +0,0 @@
-# BentoML - Machine Learning Toolkit for packaging and deploying models
-# Copyright (C) 2019 Atalaya Tech, Inc.
-
-# This program is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-
-# This program is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-# GNU General Public License for more details.
-
-# You should have received a copy of the GNU General Public License
-# along with this program.  If not, see <http://www.gnu.org/licenses/>.
-
-from __future__ import absolute_import
-from __future__ import division
-from __future__ import print_function
-
-import re
-import os
-import tempfile
-import uuid
-from datetime import datetime
-
-from abc import abstractmethod
-from bentoml.utils import Path
-from bentoml.utils.module_helper import copy_module_and_local_dependencies
-from bentoml.utils.exceptions import BentoMLException
-from bentoml.utils.s3 import is_s3_url, upload_to_s3, download_from_s3
-from bentoml.service_env import BentoServiceEnv
-from bentoml.service import BentoService
-from bentoml.artifacts import ArtifactCollection
-from bentoml.loader import load_bentoml_config
-from bentoml.version import __version__ as BENTOML_VERSION
-
-BENTO_MODEL_SETUP_PY_TEMPLATE = """\
-import os
-import pip
-import logging
-import pkg_resources
-import setuptools
-
-def _parse_requirements(file_path):
-    pip_ver = pkg_resources.get_distribution('pip').version
-    pip_version = list(map(int, pip_ver.split('.')[:2]))
-    if pip_version >= [6, 0]:
-        raw = pip.req.parse_requirements(file_path,
-                                         session=pip.download.PipSession())
-    else:
-        raw = pip.req.parse_requirements(file_path)
-    return [str(i.req) for i in raw]
-
-try:
-    install_reqs = _parse_requirements("requirements.txt")
-except Exception:
-    logging.warning('Fail load requirements file, so using default ones.')
-    install_reqs = []
-
-setuptools.setup(
-    name='{name}',
-    version='{version}',
-    description="BentoML generated model module",
-    long_description=\"\"\"{long_description}\"\"\",
-    long_description_content_type="text/markdown",
-    url="https://github.com/atalaya-io/BentoML",
-    packages=setuptools.find_packages(),
-    install_requires=install_reqs,
-    include_package_data=True,
-    package_data={{
-        '{name}': ['bentoml.yml', 'artifacts/*']
-    }},
-    # TODO: add console entry_point for using model from cli
-    # entry_points={{
-    #
-    # }}
-)
-"""
-
-MANIFEST_IN_TEMPLATE = """\
-include {model_name}/bentoml.yml
-graft {model_name}/artifacts
-"""
-
-# TODO: add setup.sh and run
-BENTO_SERVER_SINGLE_MODEL_DOCKERFILE_TEMPLATE = """\
-FROM continuumio/miniconda3
-
-ENTRYPOINT [ "/bin/bash", "-c" ]
-
-EXPOSE 5000
-
-RUN apt-get update && apt-get install -y \
-      libpq-dev \
-      build-essential \
-      && rm -rf /var/lib/apt/lists/*
-
-# copy over model files
-COPY . /model
-WORKDIR /model
-
-ARG conda_env={conda_env_name}
-
-# update conda and setup environment
-RUN conda update conda -y
-
-# create conda env
-RUN conda env create -f /model/environment.yml
-
-ENV PATH /opt/conda/envs/$conda_env/bin:$PATH
-
-RUN conda install pip && pip install -r /model/requirements.txt
-
-# Run bento server with path to model
-CMD ["bentoml serve --model-path=/model"]
-"""
-
-# TODO: improve this with import hooks PEP302?
-INIT_PY_TEMPLATE = """\
-import os
-import sys
-
-__VERSION__ = "{model_version}"
-
-_module_path = os.path.abspath(os.path.split(__file__)[0])
-
-# Prepend _module_path to sys.path, to avoid name conflicts with other installed modules
-sys.path.insert(0, _module_path)
-from {model_name}.{module_name} import {model_name}
-sys.path.remove(_module_path)
-
-# Set _bento_module_path, which tells the model where to load its artifacts
-{model_name}._bento_module_path = _module_path
-
-__all__ = ['__version__', '{model_name}']
-"""
-
-BENTOML_CONFIG_YAML_TEMPLATE = """\
-model_name: {model_name}
-model_version: {model_version}
-bentoml_version: {bentoml_version}
-module_name: {module_name}
-module_file: {module_file}
-created_at: {created_at}
-"""
-
-DEFAULT_MODEL_DESCRIPTION = """\
-# BentoML(bentoml.ai) generated model archive
-"""
-
-
-def _validate_version_str(version_str):
-    """
-    Validate that version str starts with char, has length >= 4
-    """
-    regex = r"^[a-zA-Z][a-zA-Z0-9_]{3,}"
-    if re.match(regex, version_str) is None:
-        raise ValueError('Invalid version format: "{}"'.format(version_str))
-
-
-def _generate_new_version_str():
-    """
-    Generate a version string in the format of YYYY_MM_DD_RandomHash
-    """
-    time_obj = datetime.now()
-    date_string = time_obj.strftime('%Y_%m_%d')
-    random_hash = uuid.uuid4().hex[:8]
-
-    return date_string + '_' + random_hash
-
-
-class BentoModel(BentoService):
-    """
-    BentoModel is the base abstraction for defining how a ML model can
-    be save to or load from a file directory in BentoArchive format
-    """
-
-    # User may override this in their Model definition class, to specify
-    # pip version of the genreated python module
-    _model_package_version = "1.0.0"
-
-    # User may override this if they don't want the generated model to
-    # have the same name as their Python model class name
-    _model_name = None
-
-    # This is overwritten when user install exported bento model as a
-    # pip package, in that case, #load method will load from the installed
-    # python package location
-    _bento_module_path = None
-
-    def __init__(self, *args, **kwargs):
-        self.env = BentoServiceEnv()
-        self.artifacts = ArtifactCollection()
-        self.config(self.artifacts, self.env)
-        self._version = None  # Will be set after #load or #save
-        if args or kwargs:
-            self.artifacts.pack(*args, **kwargs)
-
-        if 'module_path' in kwargs:
-            self._module_path = kwargs['module_path']
-
-        super(BentoModel, self).__init__()
-
-    @property
-    def name(self):
-        if self.__class__._model_name is not None:
-            # TODO: verify self.__class__._model_name format, can't have space in it
-            #  and can be valid folder name
-            return self.__class__._model_name
-        else:
-            return self.__class__.__name__
-
-    @property
-    def version(self):
-        return self._version
-
-    def save(self, base_path, version=None, project_base_dir=None, copy_entire_project=False):
-        if version is not None:
-            _validate_version_str(version)
-        else:
-            version = _generate_new_version_str()
-        self._version = version
-
-        if is_s3_url(base_path):
-            storage_type = 's3'
-            temp_dir = tempfile.mkdtemp()
-            remote_path = base_path
-            Path(temp_dir, self.name).mkdir(parents=True, exist_ok=True)
-            # Update path to subfolder in the form of 'base/model_name/version/'
-            path = os.path.join(temp_dir, self.name, version)
-        else:
-            storage_type = 'file'
-            Path(os.path.join(base_path), self.name).mkdir(parents=True, exist_ok=True)
-            # Update path to subfolder in the form of 'base/model_name/version/'
-            path = os.path.join(base_path, self.name, version)
-
-        if os.path.exists(path):
-            raise ValueError("Version {version} in Path: {path} already "
-                             "exist.".format(version=version, path=base_path))
-
-        os.mkdir(path)
-        module_base_path = os.path.join(path, self.name)
-        os.mkdir(module_base_path)
-
-        # write README.md with user model's docstring
-        if self.__class__.__doc__:
-            model_description = self.__class__.__doc__.strip()
-        else:
-            model_description = DEFAULT_MODEL_DESCRIPTION
-        with open(os.path.join(path, 'README.md'), 'w') as f:
-            f.write(model_description)
-
-        # save all model artifacts to 'base_path/name/artifacts/' directory
-        self.artifacts.save(module_base_path)
-
-        # write conda environment, requirement.txt
-        self.env.save(path)
-
-        # copy over all custom model code
-        module_name, module_file = copy_module_and_local_dependencies(self.__class__.__module__,
-                                                                      os.path.join(path, self.name),
-                                                                      project_base_dir,
-                                                                      copy_entire_project)
-        if os.path.isabs(module_file):
-            module_file = module_name.replace('.', os.sep) + '.py'
-
-        # create __init__.py
-        with open(os.path.join(path, self.name, '__init__.py'), "w") as f:
-            f.write(
-                INIT_PY_TEMPLATE.format(model_name=self.name, module_name=module_name,
-                                        model_version=self.__class__._model_package_version))
-
-        # write setup.py, make exported model pip installable
-        setup_py_content = BENTO_MODEL_SETUP_PY_TEMPLATE.format(
-            name=self.name, version=self.__class__._model_package_version,
-            long_description=model_description)
-        with open(os.path.join(path, 'setup.py'), 'w') as f:
-            f.write(setup_py_content)
-
-        with open(os.path.join(path, 'MANIFEST.in'), 'w') as f:
-            f.write(MANIFEST_IN_TEMPLATE.format(model_name=self.name))
-
-        # write Dockerfile
-        with open(os.path.join(path, 'Dockerfile'), 'w') as f:
-            f.write(
-                BENTO_SERVER_SINGLE_MODEL_DOCKERFILE_TEMPLATE.format(
-                    conda_env_name=self.env.get_conda_env_name()))
-
-        # write bentoml.yml
-        bentoml_yml_content = BENTOML_CONFIG_YAML_TEMPLATE.format(
-            model_name=self.name, bentoml_version=BENTOML_VERSION, model_version=version,
-            module_name=module_name, module_file=module_file, created_at=str(datetime.now()))
-        with open(os.path.join(path, 'bentoml.yml'), 'w') as f:
-            f.write(bentoml_yml_content)
-        # Also write bentoml.yml to module base path to make it accessible
-        # as package data after pip installed as a python package
-        with open(os.path.join(module_base_path, 'bentoml.yml'), 'w') as f:
-            f.write(bentoml_yml_content)
-
-        if storage_type == 's3':
-            remote_path = os.path.join(remote_path, self.name, version)
-            try:
-                upload_to_s3(remote_path, path)
-                return remote_path
-            except Exception as e:
-                raise e
-        else:
-            return path
-
-    def load(self, path=None):
-        # TODO: add model.env.verify() to check dependencies and python version etc
-
-        if self.__class__._bento_module_path is not None:
-            # When calling load from pip installled bento model, use installed
-            # python package for loading and the same path for '/artifacts'
-
-            # TODO: warn user when path is None here
-            path = self.__class__._bento_module_path
-            artifacts_path = path
-        else:
-            if path is None:
-                raise BentoMLException("Loading path is required for {}#load".format(self.name))
-
-            # When calling load on generated archive directory, look for /artifacts
-            # directory under module sub-directory
-            if is_s3_url(path):
-                temporary_path = tempfile.mkdtemp()
-                download_from_s3(path, temporary_path)
-                # Use loacl temp path for the following loading operations
-                path = temporary_path
-
-            artifacts_path = os.path.join(path, self.name)
-
-        bentoml_config = load_bentoml_config(path)
-
-        self.artifacts.load(artifacts_path)
-        self._model_name = bentoml_config['model_name']
-        self._version = bentoml_config['model_version']
-
-        return self
-
-    @abstractmethod
-    def config(self, artifacts, env):
-        pass
diff --git a/bentoml/service.py b/bentoml/service.py
index 31ed4c61..f45fa3ff 100644
--- a/bentoml/service.py
+++ b/bentoml/service.py
@@ -20,10 +20,11 @@ from __future__ import print_function
 
 import sys
 import inspect
-from abc import ABCMeta, abstractmethod
-from six import add_metaclass
 
 from bentoml.handlers import DataframeHandler
+from bentoml.utils.exceptions import BentoMLException
+from bentoml.service_env import BentoServiceEnv
+from bentoml.artifact import ArtifactCollection
 
 
 def _get_func_attr(func, attribute_name):
@@ -38,18 +39,119 @@ def _set_func_attr(func, attribute_name, value):
     return setattr(func, attribute_name, value)
 
 
-@add_metaclass(ABCMeta)
-class BentoService(object):
+# TODO(chaoyu): add property info, default to api func's doc string
+class BentoServiceAPI(object):
     """
-    BentoService is the base abstraction that exposes a list of APIs
-    for BentoAPIServer and BentoCLI
+    BentoServiceAPI defines abstraction for an API call that can be executed
+    with BentoAPIServer and BentoCLI
     """
 
-    @abstractmethod
-    def load(self, path):
+    def __init__(self, name, handler, func, options):
         """
-        Load and initialize a BentoService
+        :param name: API name
+        :param handler: A BentoHandler that transforms HTTP Request and/or
+            CLI options into parameters for API func
+        :param func: API func contains the actual API callback, this is
+            typically the 'predict' method on a model
         """
+        self._name = name
+        self._handler = handler
+        self._func = func
+        self._options = options
+
+    @property
+    def name(self):
+        return self._name
+
+    @property
+    def handler(self):
+        return self._handler
+
+    @property
+    def func(self):
+        return self._func
+
+    @property
+    def options(self):
+        return self._options
+
+
+class BentoService(object):
+    """
+    BentoService packs a list of artifacts and exposes service APIs
+    for BentoAPIServer and BentoCLI to execute, and allow customizing
+    the artifacts and environments required for the service.
+
+    >>>  from bentoml import BentoService, env
+    >>>
+    >>>  @artifacts([PickleArtifact('clf')])
+    >>>  @env(conda_dependencies: [ 'scikit-learn' ])
+    >>>  class MyMLService(BentoService):
+    >>>
+    >>>     @BentoService.api(DataframeHandler)
+    >>>     def predict(self, df):
+    >>>         return self.artifacts.clf.predict(df)
+    >>>
+    >>>  bento_service = MyMLService.pack(clf=my_trained_clf_object)
+    >>>  bentoml.save(bento_service, './export')
+    """
+
+    # User may override this if they don't want the generated model to
+    # have the same name as their Python model class name
+    _bento_service_name = None
+
+    # This is overwritten when user install exported bento model as a
+    # pip package, in that case, #load method will load from the installed
+    # python package location
+    _bento_module_path = None
+
+    def __init__(self, artifacts, env=None):
+        # TODO: validate artifacts arg matches self.__class__._artifacts_spec definition
+
+        if isinstance(artifacts, ArtifactCollection):
+            self._artifacts = artifacts
+        else:
+            self._artifacts = ArtifactCollection()
+            for artifact in artifacts:
+                self._artifacts[artifact.name] = artifact
+
+        if env is None:
+            if isinstance(self.__class__._env, dict):
+                self._env = BentoServiceEnv.fromDict(self.__class__._env)
+            else:
+                self._env = self.__class__._env
+        else:
+            # Override default model service environment
+            # This is used when bundling multiple model service together for deployment
+            self._env = env
+
+        self._config_service_apis()
+        self.name = self.__class__.name()
+
+    @property
+    def artifacts(self):
+        return self._artifacts
+
+    @property
+    def env(self):
+        return self._env
+
+    @classmethod
+    def name(cls):
+        if cls._bento_service_name is not None:
+            # TODO: verify self.__class__._bento_service_name format, can't have space in it
+            #  and can be valid folder name
+            return cls._bento_service_name
+        else:
+            # Use python class name as service name
+            return cls.__name__
+
+    @property
+    def version(self):
+        try:
+            return self._version
+        except AttributeError:
+            raise BentoMLException("Only BentoService loaded from archive has version attribute")
 
     def _config_service_apis(self):
         self._service_apis = []  # pylint:disable=attribute-defined-outside-init
@@ -62,38 +164,26 @@ class BentoService(object):
                 options = _get_func_attr(function, '_options')
                 self._service_apis.append(BentoServiceAPI(api_name, handler, func, options))
 
-    @property
-    @abstractmethod
-    def name(self):
-        pass
-
-    @property
-    @abstractmethod
-    def version(self):
-        pass
-
     def get_service_apis(self):
-        if not hasattr(self, '_service_apis'):
-            self._config_service_apis()
         return self._service_apis
 
     @staticmethod
-    def api(handler=DataframeHandler, api_name=None, options=None):
+    def api(handler, api_name=None, options=None):
         """
         Decorator for adding api to a BentoService
 
-        >>> from bentoml import BentoService
+        >>> from bentoml import BentoService, api
         >>> from bentoml.handlers import JsonHandler, DataframeHandler
         >>>
         >>> class FraudDetectionAndIdentityService(BentoService):
         >>>
-        >>>     @BentoService.api(JsonHandler)
-        >>>     def fraud_detect(self, features):
-        >>>         pass
+        >>>     @api(JsonHandler)
+        >>>     def fraud_detect(self, parsed_json):
+        >>>         # do something
         >>>
-        >>>     @BentoService.api(DataframeHandler)
-        >>>     def identity(self, features):
-        >>>         pass
+        >>>     @api(DataframeHandler)
+        >>>     def identity(self, df):
+        >>>         # do something
         """
 
         def api_decorator(func):
@@ -111,38 +201,46 @@ class BentoService(object):
 
         return api_decorator
 
+    def save(self, *args, **kwargs):
+        from bentoml import archive
+        return archive.save(self, *args, **kwargs)
 
-class BentoServiceAPI(object):
-    """
-    BentoServiceAPI defines abstraction for an API call that can be executed
-    with BentoAPIServer and BentoCLI
-    """
+    @classmethod
+    def pack(cls, *args, **kwargs):
+        if args and isinstance(args[0], ArtifactCollection):
+            return cls(args[0])
 
-    def __init__(self, name, handler, func, options):
-        """
-        :param name: API name
-        :param handler: A BentoHandler that transforms HTTP Request and/or
-            CLI options into parameters for API func
-        :param func: API func contains the actual API callback, this is
-            typically the 'predict' method on a model
-        """
-        self._name = name
-        self._handler = handler
-        self._func = func
-        self._options = options
+        artifacts = ArtifactCollection()
 
-    @property
-    def name(self):
-        return self._name
+        for artifact_spec in cls._artifacts_spec:
+            if artifact_spec.name in kwargs:
+                artifact_instance = artifact_spec.pack(kwargs[artifact_spec.name])
+                artifacts.add(artifact_instance)
 
-    @property
-    def handler(self):
-        return self._handler
+        return cls(artifacts, cls._env)
 
-    @property
-    def func(self):
-        return self._func
+    @classmethod
+    def load(cls, path=None):
+        if cls._bento_module_path is not None:
+            path = cls._bento_module_path
 
-    @property
-    def options(self):
-        return self._options
+        artifacts = ArtifactCollection.load(path, cls._artifacts_spec)
+        return cls(artifacts, cls._env)
+
+
+def artifacts(artifact_specs):
+
+    def decorator(bento_service_cls):
+        bento_service_cls._artifacts_spec = artifact_specs
+        return bento_service_cls
+
+    return decorator
+
+
+def env(**kwargs):
+
+    def decorator(bento_service_cls):
+        bento_service_cls._env = BentoServiceEnv.fromDict(kwargs)
+        return bento_service_cls
+
+    return decorator
diff --git a/bentoml/service_env.py b/bentoml/service_env.py
index 846d2971..31920975 100644
--- a/bentoml/service_env.py
+++ b/bentoml/service_env.py
@@ -42,6 +42,12 @@ CONDA_ENV_DEFAULT_NAME = 'bentoml-custom-conda-env'
 
 
 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=CONDA_ENV_DEFAULT_NAME, python_version=PYTHON_VERSION,
                  bentoml_version=BENTOML_VERSION):
@@ -58,26 +64,18 @@ class CondaEnv(object):
         return self._conda_env["name"]
 
     def add_conda_dependencies(self, extra_conda_dependencies):
-        if not isinstance(extra_conda_dependencies, list):
-            # TODO: add more validation here
-            pass
         self._conda_env["dependencies"] += extra_conda_dependencies
 
     def add_pip_dependencies(self, extra_pip_dependencies):
-        if not isinstance(extra_pip_dependencies, list):
-            # TODO: add more validation here
-            pass
         for dep in self._conda_env["dependencies"]:
             if isinstance(dep, dict) and 'pip' in dep:
                 # there is already a pip list in conda_env, append extra deps
-                dep += extra_pip_dependencies
+                dep['pip'] += extra_pip_dependencies
                 return self
+
         self._conda_env["dependencies"] += [{"pip": extra_pip_dependencies}]
 
     def add_channels(self, channels):
-        if not isinstance(channels, list):
-            # TODO: add more validation here
-            pass
         self._conda_env["channels"] += channels
 
     def write_to_yaml_file(self, filepath):
@@ -97,6 +95,15 @@ class CondaEnv(object):
 
 
 class BentoServiceEnv(object):
+    """
+    Defines all aspect of the system environment requirements for a custom
+    BentoService to be used. This includes:
+
+    conda environment - for most python third-party packages and libraries
+    requirements_txt  - for pypi dependencies that can be resolved by pip
+        when exported BentoArchieve is installed as a pypi package
+    setup_sh - for customizing the environment with user defined bash script
+    """
 
     def __init__(self):
         self._setup_sh = None
@@ -112,7 +119,6 @@ class BentoServiceEnv(object):
     def add_conda_channels(self, channels):
         if not isinstance(channels, list):
             channels = [channels]
-
         self._conda_env.add_channels(channels)
 
     def add_conda_dependencies(self, conda_dependencies):
@@ -120,34 +126,28 @@ class BentoServiceEnv(object):
             conda_dependencies = [conda_dependencies]
         self._conda_env.add_conda_dependencies(conda_dependencies)
 
-    def add_pip_dependencies(self, pip_dependencies):
+    def add_conda_pip_dependencies(self, pip_dependencies):
         if not isinstance(pip_dependencies, list):
             pip_dependencies = [pip_dependencies]
         self._conda_env.add_pip_dependencies(pip_dependencies)
 
-    def set_setup_sh(self, setup_sh_str):
-        if self._setup_sh:
-            # TODO: show warning on overwriting setup.sh
-            pass
-        self._setup_sh = setup_sh_str
-
-    def set_requirements_txt_file(self, requirements_txt_path):
-        if self._requirements_txt:
-            # TODO: show warning on overwriting requirements.txt
-            pass
-        requirements_txt_file = Path(requirements_txt_path)
+    def set_setup_sh(self, setup_sh_path_or_content):
+        setup_sh_file = Path(setup_sh_path_or_content)
+
+        if setup_sh_file.is_file():
+            with setup_sh_file.open('rb') as f:
+                self._setup_sh = f.read()
+        else:
+            self._setup_sh = setup_sh_path_or_content.encode('utf-8')
+
+    def set_requirements_txt(self, requirements_txt_path_or_content):
+        requirements_txt_file = Path(requirements_txt_path_or_content)
+
         if requirements_txt_file.is_file():
-            with requirements_txt_file.open() as f:
+            with requirements_txt_file.open('rb') as f:
                 self._requirements_txt = f.read()
         else:
-            # TODO: show error, file not exist in given path
-            pass
-
-    def set_requirements_txt_str(self, requirements_txt_str):
-        if self._requirements_txt:
-            # TODO: show warning on overwriting requirements.txt
-            pass
-        self._requirements_txt = requirements_txt_str
+            self._requirements_txt = requirements_txt_path_or_content.encode('utf-8')
 
     def save(self, path):
         conda_yml_file = os.path.join(path, 'environment.yml')
@@ -161,3 +161,24 @@ class BentoServiceEnv(object):
             setup_sh_file = os.path.join(path, 'setup.sh')
             with open(setup_sh_file, 'wb') as f:
                 f.write(self._setup_sh)
+
+    @classmethod
+    def fromDict(cls, dict):
+        env = cls()
+
+        if 'setup_sh' in dict:
+            env.set_setup_sh(dict['setup_sh'])
+
+        if 'requirements_txt' in dict:
+            env.set_requirements_txt(dict['requirements_txt'])
+
+        if 'conda_channels' in dict:
+            env.add_conda_channels(dict['conda_channels'])
+
+        if 'conda_dependencies' in dict:
+            env.add_conda_dependencies(dict['conda_dependencies'])
+
+        if 'conda_pip_dependencies' in dict:
+            env.add_conda_pip_dependencies(dict['conda_pip_dependencies'])
+
+        return env
\ No newline at end of file
diff --git a/bentoml/utils/module_helper.py b/bentoml/utils/module_helper.py
index feea9c68..cc5de73a 100644
--- a/bentoml/utils/module_helper.py
+++ b/bentoml/utils/module_helper.py
@@ -21,7 +21,6 @@ from __future__ import print_function
 import os
 import sys
 import inspect
-import shutil
 import importlib
 from modulefinder import ModuleFinder
 
@@ -35,8 +34,7 @@ def _get_module_src_file(module):
     return module.__file__[:-1] if module.__file__.endswith('.pyc') else module.__file__
 
 
-def copy_module_and_local_dependencies(target_module, destination, toplevel_package_path=None,
-                                       copy_entire_package=False):
+def copy_used_py_modules(target_module, destination):
     """
     bundle given module, and all its dependencies within top level package,
     and copy all source files to destination path, essentially creating
@@ -57,18 +55,13 @@ def copy_module_and_local_dependencies(target_module, destination, toplevel_pack
     except AttributeError:
         target_module_name = target_module.__name__
 
+    # TODO: handle when __main__ script filename starts with numbers
     target_module_file = _get_module_src_file(target_module)
 
+    # TODO: Remove this two lines?
     if target_module_name == '__main__':
         target_module_name = target_module_file[:-3].replace(os.sep, '.')
 
-    if copy_entire_package:
-        if toplevel_package_path is None:
-            raise BentoMLException("Must set toplevel_package_path when using"
-                                   "copy_entire_package=True")
-        shutil.copytree(toplevel_package_path, destination)
-        return target_module_name, target_module_file
-
     # Find all modules must be imported for target module to run
     finder = ModuleFinder()
     # NOTE: This method could take a few seconds to run
diff --git a/bentoml/version.py b/bentoml/version.py
index bd2ce904..bad172a5 100644
--- a/bentoml/version.py
+++ b/bentoml/version.py
@@ -14,4 +14,4 @@
 # You should have received a copy of the GNU General Public License
 # along with this program.  If not, see <http://www.gnu.org/licenses/>.
 
-__version__ = '0.0.7'
+__version__ = '0.0.7.dev'
diff --git a/examples/quick-start/main.py b/examples/quick-start/main.py
index 85822540..24b41b50 100644
--- a/examples/quick-start/main.py
+++ b/examples/quick-start/main.py
@@ -5,18 +5,16 @@ from sklearn import datasets
 
 # Use local bentoml code
 sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../..')))
-from bentoml import BentoModel, load, api
-from bentoml.artifacts import PickleArtifact
+from bentoml import BentoService, load, api, env, artifacts
+from bentoml.artifact import PickleArtifact
 from bentoml.handlers import JsonHandler
 
-class IrisClassifier(BentoModel):
+@artifacts([PickleArtifact('clf')])
+@env(conda_dependencies=["scikit-learn"])
+class IrisClassifier(BentoService):
     """
     Iris SVM Classifier
     """
-    
-    def config(self, artifacts, env):
-        artifacts.add(PickleArtifact('clf'))
-        env.add_conda_dependencies(["scikit-learn"])
 
     @api(JsonHandler)
     def predict(self, parsed_json):
@@ -28,7 +26,7 @@ if __name__ == "__main__":
     X, y = iris.data, iris.target
     clf.fit(X, y)
 
-    model = IrisClassifier(clf=clf)
+    model = IrisClassifier.pack(clf=clf)
     print("model.predict = {}".format(model.predict(X[0:1])))
 
     print("Saving model as bento archive...")
diff --git a/examples/sklearn-sentiment-clf/sentiment_lr_model.py b/examples/sklearn-sentiment-clf/sentiment_lr_model.py
index 9fb5ab27..5f9a8647 100644
--- a/examples/sklearn-sentiment-clf/sentiment_lr_model.py
+++ b/examples/sklearn-sentiment-clf/sentiment_lr_model.py
@@ -1,15 +1,13 @@
 import pandas as pd
 import bentoml
-from bentoml.artifacts import PickleArtifact
+from bentoml.artifact import PickleArtifact
+from bentoml.handlers import DataframeHandler
 
-class SentimentLRModel(bentoml.BentoModel):
-    
-    def config(self, artifacts, env):
-        artifacts.add(PickleArtifact('sentiment_lr'))
+@bentoml.artifacts([PickleArtifact('sentiment_lr')])
+@bentoml.env(conda_dependencies=["scikit-learn", "pandas"])
+class SentimentLRModel(bentoml.BentoService):
 
-        env.add_conda_dependencies(["scikit-learn", "pandas"])
-
-    @bentoml.api(bentoml.handlers.DataframeHandler)
+    @bentoml.api(DataframeHandler)
     def predict(self, df):
         """
         predict expects dataframe as input
diff --git a/examples/sklearn-sentiment-clf/sklearn-sentiment-clf.ipynb b/examples/sklearn-sentiment-clf/sklearn-sentiment-clf.ipynb
index 653a94eb..429aa7a4 100644
--- a/examples/sklearn-sentiment-clf/sklearn-sentiment-clf.ipynb
+++ b/examples/sklearn-sentiment-clf/sklearn-sentiment-clf.ipynb
@@ -183,16 +183,14 @@
     "%%writefile sentiment_lr_model.py\n",
     "import pandas as pd\n",
     "import bentoml\n",
-    "from bentoml.artifacts import PickleArtifact\n",
+    "from bentoml.artifact import PickleArtifact\n",
+    "from bentoml.handlers import DataframeHandler\n",
     "\n",
-    "class SentimentLRModel(bentoml.BentoModel):\n",
-    "    \n",
-    "    def config(self, artifacts, env):\n",
-    "        artifacts.add(PickleArtifact('sentiment_lr'))\n",
-    "\n",
-    "        env.add_conda_dependencies([\"scikit-learn\", \"pandas\"])\n",
+    "@bentoml.artifacts([PickleArtifact('sentiment_lr')])\n",
+    "@bentoml.env(conda_dependencies=[\"scikit-learn\", \"pandas\"])\n",
+    "class SentimentLRModel(bentoml.BentoService):\n",
     "\n",
-    "    @bentoml.api(bentoml.handlers.DataframeHandler)\n",
+    "    @bentoml.api(DataframeHandler)\n",
     "    def predict(self, df):\n",
     "        \"\"\"\n",
     "        predict expects dataframe as input\n",
@@ -209,7 +207,10 @@
     "from sentiment_lr_model import SentimentLRModel\n",
     "\n",
     "# Initialize bentoML model with artifacts\n",
-    "bento_model = SentimentLRModel(sentiment_lr=sentiment_lr)\n",
+    "\n",
+    "bento_model = SentimentLRModel.pack(\n",
+    "    sentiment_lr=sentiment_lr\n",
+    ")\n",
     "\n",
     "# Save bentoML model to directory\n",
     "saved_path = bento_model.save(\"/tmp/bento\")\n",
@@ -288,7 +289,7 @@
     "# Your bentoML model class name will become packaged name\n",
     "from SentimentLRModel import SentimentLRModel\n",
     "\n",
-    "ms = SentimentLRModel().load() # call load to ensure all artifacts are loaded\n",
+    "ms = SentimentLRModel.load() # call load to ensure all artifacts are loaded\n",
     "ms.predict([\"stupid\", \"awesome\"])"
    ]
   },


Test patch:
diff --git a/tests/test_model.py b/tests/test_model.py
index abaeaaaa..99430c64 100644
--- a/tests/test_model.py
+++ b/tests/test_model.py
@@ -4,8 +4,8 @@ import pytest
 sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..')))
 
 import bentoml
-from bentoml.model import _validate_version_str
-
+from bentoml.artifact import PickleArtifact
+from bentoml.archive import _validate_version_str
 
 def test_validate_version_str_fails():
     with pytest.raises(ValueError):
@@ -15,38 +15,37 @@ def test_validate_version_str_fails():
 def test_validate_version_str_pass():
     _validate_version_str('abc_123')
 
-class MyFakeModel(object):
+class MyTestModel(object):
     def predict(self, input):
         return int(input) * 2
 
-class MyFakeBentoModel(bentoml.BentoModel):
-    """
-    My SentimentLRModel packaging with BentoML
-    """
-
-    def config(self, artifacts, env):
-        artifacts.add(bentoml.artifacts.PickleArtifact('fake_model'))
+@bentoml.env(conda_pip_dependencies = ['scikit-learn'])
+@bentoml.artifacts([
+    PickleArtifact('model')
+])
+class MyTestBentoService(bentoml.BentoService):
 
     @bentoml.api(bentoml.handlers.DataframeHandler)
     def predict(self, df):
         """
-        predict expects dataframe as input
+        An API for testing simple bento model service
         """
-        return self.artifacts.fake_model.predict(df)
+        return self.artifacts.model.predict(df)
 
 
 BASE_TEST_PATH = "/tmp/bentoml-test"
 
 def test_save_and_load_model():
-    fake_model = MyFakeModel()
-    sm = MyFakeBentoModel(fake_model=fake_model)
-    assert sm.predict(1000) == 2000
+    test_model = MyTestModel()
+    ms = MyTestBentoService.pack(model=test_model)
+
+    assert ms.predict(1000) == 2000
 
     import uuid
     version = "test_" + uuid.uuid4().hex
-    saved_path = sm.save(BASE_TEST_PATH, version=version)
+    saved_path = ms.save(BASE_TEST_PATH, version=version)
 
-    model_path = os.path.join(BASE_TEST_PATH, 'MyFakeBentoModel', version)
+    model_path = os.path.join(BASE_TEST_PATH, 'MyTestBentoService', version)
     assert os.path.exists(model_path)
 
     model_service = bentoml.load(saved_path, lazy_load=True)
@@ -66,12 +65,11 @@ def test_save_and_load_model():
 
 @pytest.mark.skip(reason="Setup s3 creds in travis or use a mock")
 def test_save_and_load_model_from_s3():
-    fake_model = MyFakeModel()
-    sm = MyFakeBentoModel(fake_model=fake_model)
+    test_model = MyTestModel()
+    ms = MyTestBentoService.pack(model=test_model)
 
     s3_location = 's3://bentoml/test'
-    s3_saved_path = sm.save(base_path=s3_location)
-    assert s3_saved_path == os.path.join(s3_location, sm.name, sm.version)
+    s3_saved_path = ms.save(base_path=s3_location)
 
     download_model_service = bentoml.load(s3_saved_path, lazy_load=True)
     assert not download_model_service.loaded