diff --git a/README.md b/README.md index 58d3d89..276293d 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ The code has been tested using: - [Flask] (3.0): a microframework for [Python] based on Werkzeug, Jinja 2 and good intentions. - [Gunicorn] (22.0): a [Python] [WSGI] HTTP Server for UNIX. - [NGINX] (1.25): a free, open-source, high-performance HTTP server, reverse proxy, and IMAP/POP3 proxy server. -- [Docker] (26.0): an open platform for developers and sysadmins to build, ship, and run distributed applications, whether on laptops, data center VMs, or the cloud. +- [Docker] (26.1): an open platform for developers and sysadmins to build, ship, and run distributed applications, whether on laptops, data center VMs, or the cloud. - [Docker Compose] (2.26): a tool for defining and running multi-container [Docker] applications. - [Keras] ([TensorFlow] built-in): a high-level neural networks [API], written in [Python] and capable of running on top of [TensorFlow]. - [TensorFlow] (2.16): an open source software [Deep Learning] library for high performance numerical computation using data flow graphs. @@ -158,7 +158,7 @@ It is possible to execute tests of [Flask] microservice created with [pytest] fr ~/app# make test ... ============================= test session starts ============================== -platform linux -- Python 3.10.14, pytest-8.1.1, pluggy-1.5.0 +platform linux -- Python 3.10.14, pytest-8.1.2, pluggy-1.5.0 rootdir: /app/tests collected 2 items diff --git a/app/app/api.py b/app/app/api.py index be588fd..b48bbdd 100644 --- a/app/app/api.py +++ b/app/app/api.py @@ -18,10 +18,15 @@ @api.route("/predictlabel", methods=["POST"]) def predict(): - # result dictionary that will be returned from the view + """ + Predict the label of an uploaded image with the Deep Learning model. + + Returns: + dict: The JSON response with the prediction results dictionary. + """ + result = {"success": False} - # ensure an image was properly uploaded to our endpoint if request.method == "POST" and request.files.get("file"): # read image as grayscale image_req = request.files["file"].read() @@ -33,19 +38,13 @@ def predict(): # classify the input image generating a list of predictions model = current_app.config["model"] - preds = model.predict(preprocessed_image) + predictions = model.predict(preprocessed_image) # add generated predictions to result - result["predictions"] = [] - - for i in range(0, 10): - pred = {"label": str(i), "probability": str(preds[0][i])} - result["predictions"].append(pred) - - result["most_probable_label"] = str(np.argmax(preds[0])) - - # indicate that the request was a success + result["predictions"] = [ + {"label": str(i), "probability": str(pred)} for i, pred in enumerate(predictions[0]) + ] + result["most_probable_label"] = str(np.argmax(predictions[0])) result["success"] = True - # return result dictionary as JSON response to client return jsonify(result) diff --git a/app/app/model.py b/app/app/model.py index 1b204b0..aa4c950 100644 --- a/app/app/model.py +++ b/app/app/model.py @@ -13,9 +13,11 @@ def init_model(): - """Function that loads Deep Learning model. + """ + Load the pre-trained Deep Learning model. + Returns: - model: Loaded Deep Learning model. + model (tensorflow.keras.Model): The loaded Deep Learning model. """ model = load_model(current_app.config["MODEL_PATH"]) @@ -24,16 +26,22 @@ def init_model(): def preprocess_image(image): - """Function that preprocess image. + """ + Preprocess an image for the Deep Learning model. + + Args: + image (numpy.ndarray): The input image. + Returns: - image: Preprocessed image. + preprocessed_image (numpy.ndarray): The preprocessed image. """ # invert grayscale image - image = util.invert(image) - # resize and reshape image for model - image = transform.resize(image, (28, 28), anti_aliasing=True, mode="constant") - image = np.array(image) - image = image.reshape((1, 28 * 28)) + inverted_image = util.invert(image) + + # resize and reshape image + resized_image = transform.resize(inverted_image, (28, 28), anti_aliasing=True, mode="constant") + resized_image = np.array(resized_image) + preprocessed_image = resized_image.reshape((1, 28 * 28)) - return image + return preprocessed_image diff --git a/app/config.py b/app/config.py index 35026c6..fc82c4e 100644 --- a/app/config.py +++ b/app/config.py @@ -10,31 +10,54 @@ class DefaultConfig: - if os.environ.get("SECRET_KEY"): - SECRET_KEY = os.environ.get("SECRET_KEY") - else: + """ + Default configuration class. + """ + + SECRET_KEY = os.environ.get("SECRET_KEY") + if not SECRET_KEY: raise ValueError("SECRET KEY NOT FOUND!") MODEL_PATH = os.environ.get("MODEL_PATH") or "/app/mnist_model.keras" @staticmethod def init_app(app): + """ + Initialize the application with the default configuration. + """ + print("PRODUCTION CONFIG") class DevConfig(DefaultConfig): + """ + Development configuration class. + """ + DEBUG = True @classmethod def init_app(cls, app): + """ + Initialize the application with the development configuration. + """ + print("DEVELOPMENT CONFIG") class TestConfig(DefaultConfig): + """ + Testing configuration class. + """ + TESTING = True @classmethod def init_app(cls, app): + """ + Initialize the application with the testing configuration. + """ + print("TESTING CONFIG") diff --git a/app/tests/conftest.py b/app/tests/conftest.py index 6a7b69f..3cef143 100644 --- a/app/tests/conftest.py +++ b/app/tests/conftest.py @@ -13,10 +13,27 @@ @pytest.fixture def app(): + """ + Create a Flask app instance for testing. + + Returns: + flask.Flask: The Flask app instance. + """ + app = create_app("testing") return app @pytest.fixture def client(app): + """ + Create a Flask test client for the app. + + Args: + app (flask.Flask): The Flask app instance. + + Returns: + flask.testing.FlaskClient: The Flask test client. + """ + return app.test_client() diff --git a/app/tests/test_app.py b/app/tests/test_app.py index 6cdbfe8..e1f349f 100644 --- a/app/tests/test_app.py +++ b/app/tests/test_app.py @@ -10,13 +10,30 @@ def test_index(client): + """ + Test the index route. + + Args: + client (flask.testing.FlaskClient): The Flask test client. + """ + response = client.get("/") - # check response + + # assert response status assert response.status_code == 200 + + # assert response data assert response.data == b"Deep Learning on Flask" def test_api(client): + """ + Test the API endpoint to predict the label of an uploaded image with the Deep Learning model. + + Args: + client (flask.testing.FlaskClient): The Flask test client. + """ + # server REST API endpoint url and example image path SERVER_URL = "http://127.0.0.1:5000/api/predictlabel" IMAGE_PATH = "../app/static/4.jpg" @@ -26,7 +43,7 @@ def test_api(client): payload = {"file": image} response = client.post(SERVER_URL, data=payload) - # check response + # assert response status assert response.status_code == 200 # JSON format @@ -40,11 +57,13 @@ def test_api(client): if json_response["success"]: # most probable label print(json_response["most_probable_label"]) + # predictions for dic in json_response["predictions"]: print(f"label {dic['label']} probability: {dic['probability']}") + # assert the most probable label is 4 assert json_response["most_probable_label"] == "4" # failed else: - raise AssertionError() + raise AssertionError("API endpoint /predictlabel failed") diff --git a/docker-compose.yml b/docker-compose.yml index 395307c..5c0c675 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: "3.8" - services: web: build: . diff --git a/requirements.txt b/requirements.txt index cdbcc09..a88441b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,8 +2,8 @@ Flask==3.0.3 gunicorn==22.0.0 numpy==1.26.4 Pillow==10.3.0 -pytest==8.1.1 +pytest==8.1.2 requests==2.31.0 -ruff==0.4.1 +ruff==0.4.2 scikit-image==0.23.2 tensorflow==2.16.1 diff --git a/requirements_dev.txt b/requirements_dev.txt index a168825..931ce21 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -1,3 +1,3 @@ -r requirements.txt -jupyterlab==4.1.6 +jupyterlab==4.1.8 matplotlib==3.8.4