Repository: ai4os/DEEPaaS (language: python)

Issue:
Allow to load models in other ways apart from entrypoints

Add deprecation warning when various models are present



Discussion / hints:

Fix patch:
diff --git a/Dockerfile b/Dockerfile
deleted file mode 100644
index e5958dda..00000000
--- a/Dockerfile
+++ /dev/null
@@ -1,45 +0,0 @@
-FROM bitnami/minideb
-LABEL maintainer="Alvaro Lopez Garcia <aloga@ifca.unican.es>"
-LABEL version="0.10.0"
-LABEL description="DEEP as a Service Generic Container"
-
-RUN apt-get update && \
-    apt-get upgrade -y
-
-RUN apt-get install -y --no-install-recommends \
-        git \
-        curl \
-        python-netifaces \
-        python-setuptools \
-        python-pip \
-        python-wheel \
-        python3-netifaces \
-        python3-setuptools \
-        python3-pip \
-        python3-wheel 
-
-WORKDIR /srv
-
-# Install rclone
-RUN curl https://downloads.rclone.org/rclone-current-linux-amd64.deb --output rclone-current-linux-amd64.deb && \
-    dpkg -i rclone-current-linux-amd64.deb && \
-    apt-get install -f && \
-    rm rclone-current-linux-amd64.deb
-
-RUN apt-get clean && \
-    rm -rf /var/lib/apt/lists/* && \
-    rm -rf /root/.cache/pip/* && \
-    rm -rf /tmp/*
-
-## We can use pip or pip3, depending on the python version that we want to use
-RUN pip3 install 'deepaas>=0.4.0' && \
-    pip install 'deepaas>=0.4.0'
-
-EXPOSE 5000
-
-# Do not run DEEPaaS within a shell (i.e. using "sh -c") here below, 
-# as the shell will become PID 1, and this will cause that the Docker
-# container will not stop on a "docker stop" command, as Docker sends
-# SIGTERM to the PID 1 (the shell will not propagate the signal to 
-# the child process.
-CMD ["deepaas-run", "--listen-ip", "0.0.0.0", "--listen-port", "5000"]
diff --git a/deepaas/api/v2/predict.py b/deepaas/api/v2/predict.py
index 9ee4aea9..8141469e 100644
--- a/deepaas/api/v2/predict.py
+++ b/deepaas/api/v2/predict.py
@@ -16,7 +16,6 @@
 
 from aiohttp import web
 import aiohttp_apispec
-import marshmallow
 from webargs import aiohttpparser
 import webargs.core
 
@@ -39,9 +38,7 @@ def _get_handler(model_name, model_obj):
     accept = aux.get("accept", None)
     if accept:
         accept.validate.choices.append("*/*")
-        # If no default value use first possible choice:
-        if isinstance(accept.missing, marshmallow.utils._Missing):
-            accept.missing = accept.validate.choices[0]
+        accept.load_default = accept.validate.choices[0]
         accept.location = "headers"
 
     handler_args = webargs.core.dict2schema(aux)
diff --git a/deepaas/cmd/cli.py b/deepaas/cmd/cli.py
index 012ef721..7ff9d8bb 100644
--- a/deepaas/cmd/cli.py
+++ b/deepaas/cmd/cli.py
@@ -34,9 +34,11 @@
 from oslo_log import log
 
 # from deepaas import config
+from deepass import config
 from deepaas.model import loading
 from deepaas.model.v2 import wrapper as v2_wrapper
 
+CONF = config.CONF
 
 debug_cli = False
 
@@ -152,15 +154,14 @@ def _get_model_name(model_name=None):
         sys.stderr.write(
             "[ERROR]: There are several models available ({}).\n"
             "You have to choose one and set it in the DEEPAAS_V2_MODEL "
-            "environment setting.\n".format(list(models.keys()))
+            "environment variable or using the --mode-name option"
+            ".\n".format(list(models.keys()))
         )
         sys.exit(1)
 
 
 # Get the model name
-model_name = None
-if "DEEPAAS_V2_MODEL" in os.environ:
-    model_name = os.environ["DEEPAAS_V2_MODEL"]
+model_name = CONF.model_name
 
 model_name, model_obj = _get_model_name(model_name)
 
diff --git a/deepaas/cmd/execute.py b/deepaas/cmd/execute.py
index 224751db..7ae12326 100644
--- a/deepaas/cmd/execute.py
+++ b/deepaas/cmd/execute.py
@@ -26,17 +26,6 @@
 from deepaas.model.v2.wrapper import UploadedFile
 
 cli_opts = [
-    cfg.StrOpt(
-        "model-name",
-        help="""
-Add the name of the model from which you want
-to obtain the prediction.
-If there are multiple models installed and youd don't
-specify the name of the one you want to use the program will fail.
-If there is only one model installed, that will be used
-to make the prediction.
-""",
-    ),
     cfg.StrOpt(
         "input-file",
         short="i",
diff --git a/deepaas/config.py b/deepaas/config.py
index f87a2c18..6d85997b 100644
--- a/deepaas/config.py
+++ b/deepaas/config.py
@@ -15,6 +15,7 @@
 # under the License.
 
 import logging
+import os
 import warnings
 
 from oslo_config import cfg
@@ -85,6 +86,19 @@
 Pre-warm the modules (eg. load models, do preliminary checks, etc). You might
 want to disable this option if DEEPaaS is loading more than one module because
 you risk getting out of memory errors.
+""",
+    ),
+    cfg.StrOpt(
+        "model-name",
+        default=os.environ.get("DEEPAAS_V2_MODEL", ""),
+        help="""
+Specify the model to be used. If not specified, DEEPaaS will serve all the models that
+are available. If specified, DEEPaaS will serve only the specified model. You can also
+use the DEEPAAS_V2_MODEL environment variable.
+
+WARNING: Serving multiple models is deprecated and will be removed in the future,
+         therefore it is strongly suggested that you specify the model you want to
+         or that you ensure that only one model is available.
 """,
     ),
 ]
diff --git a/deepaas/exceptions.py b/deepaas/exceptions.py
new file mode 100644
index 00000000..2f15fb3b
--- /dev/null
+++ b/deepaas/exceptions.py
@@ -0,0 +1,19 @@
+# -*- coding: utf-8 -*-
+
+# Copyright 2018 Spanish National Research Council (CSIC)
+#
+# 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.
+
+
+class ModuleNotFoundError(Exception):
+    """Module not found error."""
diff --git a/deepaas/model/loading.py b/deepaas/model/loading.py
index 62f351b8..55441870 100644
--- a/deepaas/model/loading.py
+++ b/deepaas/model/loading.py
@@ -14,6 +14,8 @@
 # License for the specific language governing permissions and limitations
 # under the License.
 
+from deepaas import exceptions
+
 import stevedore
 
 NAMESPACES = {
@@ -21,6 +23,28 @@
 }
 
 
+def get_model_by_name(name, version):
+    """Get a model by its name.
+
+    :param name: The name of the model.
+    :type name: str
+    :param version: The version of the model.
+    :type version: str
+
+    :returns: The model.
+    :rtype: object
+    """
+    mgr = stevedore.NamedExtensionManager(
+        namespace=NAMESPACES.get(version),
+        names=[name],
+    )
+    if name not in mgr.names():
+        raise exceptions.ModuleNotFoundError(
+            "Model '%s' not found in namespace '%s'" % (name, NAMESPACES.get(version))
+        )
+    return mgr[name].plugin
+
+
 def get_available_model_names(version):
     """Get the names of all the models that are available on the system.
 
diff --git a/deepaas/model/v2/__init__.py b/deepaas/model/v2/__init__.py
index f36e0638..920cc01b 100644
--- a/deepaas/model/v2/__init__.py
+++ b/deepaas/model/v2/__init__.py
@@ -14,14 +14,20 @@
 # License for the specific language governing permissions and limitations
 # under the License.
 
+import warnings
+
 from oslo_log import log
 
+from deepaas import config
+from deepaas import exceptions
 from deepaas.model import loading
 from deepaas.model.v2 import test
 from deepaas.model.v2 import wrapper
 
 LOG = log.getLogger(__name__)
 
+CONF = config.CONF
+
 # Model registry
 MODELS = {}
 MODELS_LOADED = False
@@ -35,17 +41,47 @@ def register_models(app):
         return
 
     try:
-        for name, model in loading.get_available_models("v2").items():
-            MODELS[name] = wrapper.ModelWrapper(name, model, app)
+        if CONF.model_name:
+            MODELS[CONF.model_name] = wrapper.ModelWrapper(
+                CONF.model_name,
+                loading.get_model_by_name(CONF.model_name, "v2"),
+                app,
+            )
+        else:
+            for name, model in loading.get_available_models("v2").items():
+                MODELS[name] = wrapper.ModelWrapper(name, model, app)
+    except exceptions.ModuleNotFoundError:
+        LOG.error("Model not found: %s", CONF.model_name)
+        raise
     except Exception as e:
+        # We do not raise here, as we have not yet removed the deprecated loading of the
+        # test module... but we should remove it as soon as the code below is deprecated
         LOG.warning("Error loading models: %s", e)
+        warnings.warn(
+            "Error loading models, using test model. This will be deprecated soon.",
+            DeprecationWarning,
+        )
 
     if MODELS:
+        if len(MODELS) > 1:
+            # Loading several models will be deprecated in the future
+            warn_msg = "Loading several models is deprecated."
+            warnings.warn(warn_msg, DeprecationWarning)
+            LOG.warning(warn_msg)
+
         MODELS_LOADED = True
         return
 
     if not MODELS:
+        # Raise deprecation warning
+        warn_msg = (
+            "Using the built-in test model is deprecated, if you are testing the "
+            "API, please use the demo_app instead. "
+            "Check https://github.com/deephdc/demo_app for more information.",
+        )
+        warnings.warn(warn_msg, DeprecationWarning)
         LOG.info("No models found in V2, loading test model")
+        LOG.warning(warn_msg)
         MODELS["deepaas-test"] = wrapper.ModelWrapper(
             "deepaas-test", test.TestModel(), app
         )
diff --git a/doc/source/cli/deepaas-cli.rst b/doc/source/cli/deepaas-cli.rst
index b86dcd03..b194e79f 100644
--- a/doc/source/cli/deepaas-cli.rst
+++ b/doc/source/cli/deepaas-cli.rst
@@ -10,20 +10,20 @@ Synopsis
 Description
 ===========
 
-:program:`deepaas-cli` Command line interface (CLI) to DEEPaaS models 
+:program:`deepaas-cli` Command line interface (CLI) to DEEPaaS models
     that are loaded through the ``deepaas.v2.models`` entrypoint API.
-    One gets access to the same get_metadata, warm, predict, and train 
+    One gets access to the same get_metadata, warm, predict, and train
     methods with all corresponding options as with DEEPaaS REST API.
-    To get available for the method options, one has to call 
+    To get available for the method options, one has to call
     ``deepaas-cli <method> --help``.
     Additional parameters are provided:
-    ``--deepaas_method_output`` is to store the output in a pre-defined 
-    by a user file; 
-    ``--deepaas_with_multiprocessing`` is to activate multiprocessing 
+    ``--deepaas_method_output`` is to store the output in a pre-defined
+    by a user file;
+    ``--deepaas_with_multiprocessing`` is to activate multiprocessing
     support, default is True.
-    oslo_log package is used for logging information, which provides 
+    oslo_log package is used for logging information, which provides
     additional options for the script.
-    If several models are available for loading, one has to provide 
+    If several models are available for loading, one has to provide
     which one to load via DEEPAAS_V2_MODEL environment setting.
 
 Options
@@ -42,21 +42,31 @@ Options
 
    Calls predict() method. The output can be stored via
    --deepaas_method_output.
-   
+
 .. option:: train
 
    Calls train() method. The output can be stored via
    --deepaas_method_output.
 
-.. option:: --deepaas_method_output 
+.. option:: --deepaas_method_output
 
-   To save the results to a local file, if needed. Available for 
+   To save the results to a local file, if needed. Available for
    get_metadata, predict, train methods.
 
-.. option:: --deepaas_with_multiprocessing 
+.. option:: --deepaas_with_multiprocessing
 
    To activate multiprocessing support, default is True.
-  
+
+.. option:: --model-name MODEL_NAME
+
+    Specify the model to be used. If not specified, DEEPaaS will serve all the models
+    that are available. If specified, DEEPaaS will serve only the specified model. You
+    can also use the DEEPAAS_V2_MODEL environment variable.
+
+    WARNING: Serving multiple models is deprecated and will be removed in the future,
+    therefore it is strongly suggested that you specify the model you want to or that
+    you ensure that only one model is available.
+
 Files
 =====
 
diff --git a/doc/source/cli/deepaas-predict.rst b/doc/source/cli/deepaas-predict.rst
index 53dee532..469395d3 100644
--- a/doc/source/cli/deepaas-predict.rst
+++ b/doc/source/cli/deepaas-predict.rst
@@ -10,8 +10,8 @@ Synopsis
 Description
 ===========
 
-:program:`deepaas-predict` It is a command that allows you to obtain, 
-    through the command line, the prediction of a file or the url of 
+:program:`deepaas-predict` It is a command that allows you to obtain,
+    through the command line, the prediction of a file or the url of
     a file, of the models that are loaded through the ``deepaas.v2.models``
     entrypoint API.
 
@@ -28,23 +28,24 @@ Options
    option must be available in the model used.
    (by default application/json).
 
-.. option:: --model-name CONTENT_TYPE, -c CONTENT_TYPE
+.. option:: --model-name MODEL_NAME
+
+    Specify the model to be used. If not specified, DEEPaaS will serve all the models
+    that are available. If specified, DEEPaaS will serve only the specified model. You
+    can also use the DEEPAAS_V2_MODEL environment variable.
+
+    WARNING: Serving multiple models is deprecated and will be removed in the future,
+    therefore it is strongly suggested that you specify the model you want to or that
+    you ensure that only one model is available.
 
-   Add the name of the model from which you want
-   to obtain the prediction.
-   If there are multiple models installed and youd don't
-   specify the name of the one you want to use the program will fail.
-   If there is only one model installed, that will be used
-   to make the prediction.
-   
 .. option:: --output OUTPUT_DIR, -o OUTPUT_DIR
 
    Save the result to a local file. This option is required.
 
-.. option:: --url, -u 
+.. option:: --url, -u
 
    If set to true, the input file is the url o a file to predict.
-  
+
 Files
 =====
 
diff --git a/doc/source/cli/deepaas-run.rst b/doc/source/cli/deepaas-run.rst
index 421d4d48..9ce3043c 100644
--- a/doc/source/cli/deepaas-run.rst
+++ b/doc/source/cli/deepaas-run.rst
@@ -16,6 +16,16 @@ Description
 Options
 =======
 
+.. option:: --model-name MODEL_NAME
+
+    Specify the model to be used. If not specified, DEEPaaS will serve all the models
+    that are available. If specified, DEEPaaS will serve only the specified model. You
+    can also use the DEEPAAS_V2_MODEL environment variable.
+
+    WARNING: Serving multiple models is deprecated and will be removed in the future,
+    therefore it is strongly suggested that you specify the model you want to or that
+    you ensure that only one model is available.
+
 .. option:: --debug, -d
 
    If set to true, the logging level will be set to DEBUG instead of the
diff --git a/doc/source/user/v2-api.rst b/doc/source/user/v2-api.rst
index 0c67c913..a4a1960b 100644
--- a/doc/source/user/v2-api.rst
+++ b/doc/source/user/v2-api.rst
@@ -15,6 +15,12 @@ loaded to offer the model functionality through the API. This allows you to
 offer several models using a single DEEPaaS instance, by defining different
 entry points for the different models.
 
+.. warning::
+   Serving multiple models is marked as deprecated, and will be removed in a
+   future major version of the API. Please ensure that you start using the `model-name`
+   configuration option in your configuration file or the `--model-name` command line
+   option as soon as possible.
+
 .. _Setuptools: https://setuptools.readthedocs.io/en/latest/setuptools.html
 
 When the DEEPaaS API is spawned it will look for the ``deepaas.v2.model``
diff --git a/releasenotes/notes/deprecate-old-model-loading-27aa4485b2132da7.yaml b/releasenotes/notes/deprecate-old-model-loading-27aa4485b2132da7.yaml
new file mode 100644
index 00000000..221d3863
--- /dev/null
+++ b/releasenotes/notes/deprecate-old-model-loading-27aa4485b2132da7.yaml
@@ -0,0 +1,10 @@
+---
+prelude: >
+    Model loading is being refactored, so that only a single model will be
+    available for serving.
+deprecations:
+  - Loading the default deepaas-test model if no models are available is now
+    marked as deprecated and will be removed in the next major version.
+  - Loading several models is now marked as deprecated. If you have several models
+    installed, please use the `model-name` configuration option or the
+    `--model-name` CLI option.


Test patch:
diff --git a/deepaas/model/v2/test.py b/deepaas/model/v2/test.py
index c0888f0b..c045dced 100644
--- a/deepaas/model/v2/test.py
+++ b/deepaas/model/v2/test.py
@@ -111,30 +111,37 @@ def train(self, *args, **kwargs):
     def get_predict_args(self):
         return {
             "data": fields.Field(
-                description="Data file to perform inference.",
+                metadata={
+                    "description": "Data file to perform inference.",
+                    "location": "form",
+                    "type": "file",
+                },
                 required=True,
-                location="form",
-                type="file",
             ),
             "parameter": fields.Int(
-                description="This is a parameter for prediction", required=True
+                metadata={"description": "This is a parameter for prediction"},
+                required=True,
             ),
             "parameter_three": fields.Str(
-                description=(
-                    "This is a parameter that forces its value to "
-                    "be one of the choices declared in 'enum'"
-                ),
-                enum=["foo", "bar"],
+                metadata={
+                    "description": (
+                        "This is a parameter that forces its value to "
+                        "be one of the choices declared in 'enum'"
+                    ),
+                    "enum": ["foo", "bar"],
+                },
                 validate=validate.OneOf(["foo", "bar"]),
             ),
             "accept": fields.Str(
-                description=(
-                    "Media type(s) that is/are acceptable for the " "response."
-                ),
+                metadata={
+                    "description": (
+                        "Media type(s) that is/are acceptable for the " "response."
+                    ),
+                    "location": "headers",
+                },
                 validate=validate.OneOf(
                     ["application/json", "text/plain", "image/png"]
                 ),
-                location="headers",
             ),
         }
 
@@ -142,9 +149,15 @@ def get_train_args(self):
         return {
             "sleep": fields.Int(
                 required=True,
-                descripton="This is a integer parameter, and it is " "a required one.",
+                metadata={
+                    "descripton": (
+                        "This is a integer parameter, and it is " "a required one."
+                    ),
+                },
+            ),
+            "parameter_two": fields.Str(
+                metadata={"description": "This is a string parameter."}
             ),
-            "parameter_two": fields.Str(description="This is a string parameter."),
         }
 
     def get_metadata(self):
diff --git a/deepaas/tests/base.py b/deepaas/tests/base.py
index 7dc9cb78..c6965429 100644
--- a/deepaas/tests/base.py
+++ b/deepaas/tests/base.py
@@ -32,7 +32,6 @@
 
 
 class TestCase(testtools.TestCase, aiohttp.test_utils.AioHTTPTestCase):
-
     """Base unit test class."""
 
     async def get_application(self):