From f181a7fedfdb199aea493e54aacb112934ddbb07 Mon Sep 17 00:00:00 2001 From: Alex Bozarth Date: Thu, 28 Feb 2019 17:18:01 -0800 Subject: [PATCH 1/5] Restore labels endpoint and embedded app Reimplemented the `/model/labels` endpoint and embedded web app lost in the transition to the MAX Framework This update is dependant on the purposed version 1.1.0 of MAX Framework --- api/__init__.py | 1 + api/labels.py | 27 +++++++++++++++++++++++++++ app.py | 4 +++- 3 files changed, 31 insertions(+), 1 deletion(-) create mode 100644 api/labels.py diff --git a/api/__init__.py b/api/__init__.py index 2cc4309..a2dba3e 100644 --- a/api/__init__.py +++ b/api/__init__.py @@ -1,2 +1,3 @@ from .metadata import ModelMetadataAPI # noqa +from .labels import ModelLabelsAPI # noqa from .predict import ModelPredictAPI # noqa diff --git a/api/labels.py b/api/labels.py new file mode 100644 index 0000000..31f194a --- /dev/null +++ b/api/labels.py @@ -0,0 +1,27 @@ +from maxfw.core import MAX_API, LabelsAPI +from flask_restplus import fields +from core.model import ModelWrapper + +model_label = MAX_API.model('ModelLabel', { + 'id': fields.String(required=True, description='Class label identifier'), + 'name': fields.String(required=True, description='Class label'), +}) + +labels_response = MAX_API.model('LabelsResponse', { + 'count': fields.Integer(required=True, description='Number of class labels returned'), + 'labels': fields.List(fields.Nested(model_label), description='Class labels that can be predicted by the model') +}) + + +class ModelLabelsAPI(LabelsAPI): + + model_wrapper = ModelWrapper() + + @MAX_API.doc('labels') + @MAX_API.marshal_with(labels_response) + def get(self): + '''Return the list of labels that can be predicted by the model''' + result = {} + result['labels'] = self.model_wrapper.categories + result['count'] = len(self.model_wrapper.categories) + return result diff --git a/app.py b/app.py index 38776fc..40a48b2 100644 --- a/app.py +++ b/app.py @@ -1,8 +1,10 @@ from maxfw.core import MAXApp -from api import ModelMetadataAPI, ModelPredictAPI +from api import ModelMetadataAPI, ModelLabelsAPI, ModelPredictAPI from config import API_TITLE, API_DESC, API_VERSION max_app = MAXApp(API_TITLE, API_DESC, API_VERSION) max_app.add_api(ModelMetadataAPI, '/metadata') +max_app.add_api(ModelLabelsAPI, '/labels') max_app.add_api(ModelPredictAPI, '/predict') +max_app.mount_static('/app/') max_app.run() From 1bbe124ec955681eecf753f8635d2b23523e14e2 Mon Sep 17 00:00:00 2001 From: Alex Bozarth Date: Fri, 1 Mar 2019 13:13:14 -0800 Subject: [PATCH 2/5] Updated ModelLabelsAPI to match changes to maxfw Cleaned up labels.py: - to pass flake - match the latest maxfw code from the parent PR - simplify code Also added local build dirs to git ignore --- .gitignore | 4 ++++ api/labels.py | 21 ++++++++++++--------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/.gitignore b/.gitignore index cd0d43a..128f90a 100644 --- a/.gitignore +++ b/.gitignore @@ -105,3 +105,7 @@ ENV/ .mypy_cache/ .idea/ .pytest_cache/ + +# local build files +static/ +assets/data/ \ No newline at end of file diff --git a/api/labels.py b/api/labels.py index 31f194a..b43be3d 100644 --- a/api/labels.py +++ b/api/labels.py @@ -1,4 +1,4 @@ -from maxfw.core import MAX_API, LabelsAPI +from maxfw.core import MAX_API, MAXAPI from flask_restplus import fields from core.model import ModelWrapper @@ -8,20 +8,23 @@ }) labels_response = MAX_API.model('LabelsResponse', { - 'count': fields.Integer(required=True, description='Number of class labels returned'), - 'labels': fields.List(fields.Nested(model_label), description='Class labels that can be predicted by the model') + 'count': fields.Integer(required=True, + description='Number of class labels returned'), + 'labels': fields.List(fields.Nested(model_label), + description='Class labels that can be predicted by ' + 'the model') }) -class ModelLabelsAPI(LabelsAPI): +class ModelLabelsAPI(MAXAPI): model_wrapper = ModelWrapper() @MAX_API.doc('labels') @MAX_API.marshal_with(labels_response) def get(self): - '''Return the list of labels that can be predicted by the model''' - result = {} - result['labels'] = self.model_wrapper.categories - result['count'] = len(self.model_wrapper.categories) - return result + """Return the list of labels that can be predicted by the model""" + return { + 'labels': self.model_wrapper.categories, + 'count': len(self.model_wrapper.categories) + } From 8505cd69176505fee78b42afd0792eb58cda48c8 Mon Sep 17 00:00:00 2001 From: Alex Bozarth Date: Mon, 4 Mar 2019 13:50:54 -0800 Subject: [PATCH 3/5] Updated to match latest maxfw pr changes --- api/labels.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/labels.py b/api/labels.py index b43be3d..d3dc50e 100644 --- a/api/labels.py +++ b/api/labels.py @@ -1,4 +1,4 @@ -from maxfw.core import MAX_API, MAXAPI +from maxfw.core import MAX_API, CustomMAXAPI from flask_restplus import fields from core.model import ModelWrapper @@ -16,7 +16,7 @@ }) -class ModelLabelsAPI(MAXAPI): +class ModelLabelsAPI(CustomMAXAPI): model_wrapper = ModelWrapper() From 124df84db12fa2ddf6d8c5697acce00996148ced Mon Sep 17 00:00:00 2001 From: Alex Bozarth Date: Tue, 5 Mar 2019 12:19:47 -0800 Subject: [PATCH 4/5] moved ModelLabelsAPI code into predict.py --- api/__init__.py | 3 +-- api/labels.py | 30 ------------------------------ api/predict.py | 36 +++++++++++++++++++++++++++++++----- 3 files changed, 32 insertions(+), 37 deletions(-) delete mode 100644 api/labels.py diff --git a/api/__init__.py b/api/__init__.py index a2dba3e..0155d24 100644 --- a/api/__init__.py +++ b/api/__init__.py @@ -1,3 +1,2 @@ from .metadata import ModelMetadataAPI # noqa -from .labels import ModelLabelsAPI # noqa -from .predict import ModelPredictAPI # noqa +from .predict import ModelLabelsAPI, ModelPredictAPI # noqa diff --git a/api/labels.py b/api/labels.py deleted file mode 100644 index d3dc50e..0000000 --- a/api/labels.py +++ /dev/null @@ -1,30 +0,0 @@ -from maxfw.core import MAX_API, CustomMAXAPI -from flask_restplus import fields -from core.model import ModelWrapper - -model_label = MAX_API.model('ModelLabel', { - 'id': fields.String(required=True, description='Class label identifier'), - 'name': fields.String(required=True, description='Class label'), -}) - -labels_response = MAX_API.model('LabelsResponse', { - 'count': fields.Integer(required=True, - description='Number of class labels returned'), - 'labels': fields.List(fields.Nested(model_label), - description='Class labels that can be predicted by ' - 'the model') -}) - - -class ModelLabelsAPI(CustomMAXAPI): - - model_wrapper = ModelWrapper() - - @MAX_API.doc('labels') - @MAX_API.marshal_with(labels_response) - def get(self): - """Return the list of labels that can be predicted by the model""" - return { - 'labels': self.model_wrapper.categories, - 'count': len(self.model_wrapper.categories) - } diff --git a/api/predict.py b/api/predict.py index 2ace8c7..3761595 100644 --- a/api/predict.py +++ b/api/predict.py @@ -1,8 +1,36 @@ -from maxfw.core import MAX_API, PredictAPI +from maxfw.core import MAX_API, PredictAPI, CustomMAXAPI from flask_restplus import fields from werkzeug.datastructures import FileStorage from core.model import ModelWrapper +model_label = MAX_API.model('ModelLabel', { + 'id': fields.String(required=True, description='Class label identifier'), + 'name': fields.String(required=True, description='Class label'), +}) + +labels_response = MAX_API.model('LabelsResponse', { + 'count': fields.Integer(required=True, + description='Number of class labels returned'), + 'labels': fields.List(fields.Nested(model_label), + description='Class labels that can be predicted by ' + 'the model') +}) + +model_wrapper = ModelWrapper() + + +class ModelLabelsAPI(CustomMAXAPI): + + @MAX_API.doc('labels') + @MAX_API.marshal_with(labels_response) + def get(self): + """Return the list of labels that can be predicted by the model""" + return { + 'labels': model_wrapper.categories, + 'count': len(model_wrapper.categories) + } + + input_parser = MAX_API.parser() input_parser.add_argument('image', type=FileStorage, location='files', required=True, help='An image file (encoded as PNG or JPG/JPEG)') @@ -25,8 +53,6 @@ class ModelPredictAPI(PredictAPI): - model_wrapper = ModelWrapper() - @MAX_API.doc('predict') @MAX_API.expect(input_parser) @MAX_API.marshal_with(predict_response) @@ -37,8 +63,8 @@ def post(self): args = input_parser.parse_args() threshold = args['threshold'] image_data = args['image'].read() - image = self.model_wrapper._read_image(image_data) - label_preds = self.model_wrapper._predict(image, threshold) + image = model_wrapper._read_image(image_data) + label_preds = model_wrapper._predict(image, threshold) result['predictions'] = label_preds result['status'] = 'ok' From ecc48fa2eb8312be9a99594a7519de2ad0a14ba1 Mon Sep 17 00:00:00 2001 From: Alex Bozarth Date: Tue, 5 Mar 2019 21:06:27 -0800 Subject: [PATCH 5/5] Update max-base version to include upstream PRs --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 99c23b6..07cc4c7 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM codait/max-base:v1.1.0 +FROM codait/max-base:v1.1.1 ARG model_bucket=http://max-assets.s3-api.us-geo.objectstorage.softlayer.net/object-detector/1.0 ARG model_file=model.tar.gz