From d2bffe94616f6dda7cd4ac603f3fab64001e6735 Mon Sep 17 00:00:00 2001 From: Arun Sureddi Date: Tue, 12 Nov 2024 11:58:06 -0800 Subject: [PATCH 01/12] Deprecate the compatibility layer for Flask 1.0 --- .../server/config.py | 4 -- .../server/create_app.py | 44 ------------------- docs/AzureMLInferenceServer.md | 1 - setup.py | 4 +- 4 files changed, 2 insertions(+), 51 deletions(-) diff --git a/azureml_inference_server_http/server/config.py b/azureml_inference_server_http/server/config.py index 8dc200f..bac004b 100644 --- a/azureml_inference_server_http/server/config.py +++ b/azureml_inference_server_http/server/config.py @@ -29,7 +29,6 @@ "SERVICE_PATH_PREFIX": "service_path_prefix", "SERVICE_VERSION": "service_version", "SCORING_TIMEOUT_MS": "scoring_timeout", - "AML_FLASK_ONE_COMPATIBILITY": "flask_one_compatibility", "AZUREML_LOG_LEVEL": "log_level", "AML_APP_INSIGHTS_ENABLED": "app_insights_enabled", "AML_APP_INSIGHTS_KEY": "app_insights_key", @@ -119,9 +118,6 @@ class AMLInferenceServerConfig(BaseSettings): # Dictates how long scoring function with run before timeout in milliseconds. scoring_timeout: int = pydantic.Field(default=3600 * 1000, alias="SCORING_TIMEOUT_MS") - # When @rawhttp is used, whether the user requires on `request` object to have the flask v1 properties/behavior. - flask_one_compatibility: bool = pydantic.Field(default=True) - # Sets the Logging level log_level: str = pydantic.Field(default="INFO", alias="AZUREML_LOG_LEVEL") diff --git a/azureml_inference_server_http/server/create_app.py b/azureml_inference_server_http/server/create_app.py index 5ac283b..3bcdb0c 100644 --- a/azureml_inference_server_http/server/create_app.py +++ b/azureml_inference_server_http/server/create_app.py @@ -20,50 +20,6 @@ logger = logging.getLogger("azmlinfsrv") - -def patch_flask(): - # While "packaging" is the recommended package for comparing versions, we can't introduce the dependency because - # we need our server to work even when the user doesn't have latest dependencies installed. - with warnings.catch_warnings(): - warnings.filterwarnings( - action="ignore", - category=DeprecationWarning, - message="distutils Version classes are deprecated.", - ) - patch_werkzeug = LooseVersion(werkzeug.__version__) >= LooseVersion("2.1") - - if patch_werkzeug: - # Request.headers.has_key() was removed in werkzeug 2.1 - # https://github.com/pallets/werkzeug/commit/03979aaff2b8020fd6fd52e69745950d484e3fa5 - # Restore the functionality to preserve backwards compatability. - werkzeug.datastructures.EnvironHeaders.has_key = werkzeug.datastructures.EnvironHeaders.__contains__ - werkzeug.datastructures.CombinedMultiDict.has_key = werkzeug.datastructures.CombinedMultiDict.__contains__ - - # In werkzeug 2.1, get_json() is modified to throw BadRequest if the content type is not application/json. - @functools.wraps(flask.Request.on_json_loading_failed) - def on_json_loading_failed(self, e): - if e is None: - return None - - return on_json_loading_failed.__wrapped__(self, e) - - flask.Request.on_json_loading_failed = on_json_loading_failed - logger.info("AML_FLASK_ONE_COMPATIBILITY is set. Patched Flask to ensure compatibility with Flask 1.") - else: - logger.info("AML_FLASK_ONE_COMPATIBILITY is set, but patching is not necessary.") - - -if config.flask_one_compatibility: - try: - patch_flask() - except Exception: - logger.warning( - "AML_FLASK_ONE_COMPATIBILITY is set. However, compatibility patch for Flask 1 has failed. " - "This is only a problem if you use @rawhttp and relies on deprecated methods such as has_key().\n" - + traceback.format_exc() - ) - - def create(): app = Flask(__name__) # To create a new instance of main_blueprint each time the create() is called diff --git a/docs/AzureMLInferenceServer.md b/docs/AzureMLInferenceServer.md index d7dfea9..402c2ef 100644 --- a/docs/AzureMLInferenceServer.md +++ b/docs/AzureMLInferenceServer.md @@ -247,7 +247,6 @@ Config file will support only below keys: | SERVICE_PATH_PREFIX | No | "" | | SERVICE_VERSION | No| "1.0" | | SCORING_TIMEOUT_MS | No | 3600 * 1000 | -| AML_FLASK_ONE_COMPATIBILITY | No | "True" | | AZUREML_LOG_LEVEL | No | "INFO" | | AML_APP_INSIGHTS_ENABLED | No | False | | AML_APP_INSIGHTS_KEY | No | None | diff --git a/setup.py b/setup.py index 3b25cdc..de6633a 100644 --- a/setup.py +++ b/setup.py @@ -65,7 +65,7 @@ def get_license(): ], python_requires=">=3.8", install_requires=[ - "flask<=2.3.2", # We aim to be compatible with both flask 1 and 2 + "flask~=3.0.0", "flask-cors~=5.0.0", 'gunicorn>=23.0.0; platform_system!="Windows"', "inference-schema~=1.8.0", @@ -73,7 +73,7 @@ def get_license(): 'psutil<6.0.0; platform_system=="Windows"', "pydantic~=2.9.0", "pydantic-settings", - 'waitress==2.1.2; platform_system=="Windows"', + 'waitress>=3.0.1; platform_system=="Windows"', "werkzeug>=3.0.3", # Werkzeug 3.x breaks back-compatibility of urls package "certifi>=2024.7.4", # Python (Pip) Security Update for certifi (GHSA-248v-346w-9cwc) ], From 56ecf507557281284018438c57ea0f65a5de9cad Mon Sep 17 00:00:00 2001 From: Arun Sureddi Date: Tue, 12 Nov 2024 12:00:28 -0800 Subject: [PATCH 02/12] Update waitress package version for windows installation --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 3b25cdc..5723f3e 100644 --- a/setup.py +++ b/setup.py @@ -73,7 +73,7 @@ def get_license(): 'psutil<6.0.0; platform_system=="Windows"', "pydantic~=2.9.0", "pydantic-settings", - 'waitress==2.1.2; platform_system=="Windows"', + 'waitress>=3.0.1; platform_system=="Windows"', "werkzeug>=3.0.3", # Werkzeug 3.x breaks back-compatibility of urls package "certifi>=2024.7.4", # Python (Pip) Security Update for certifi (GHSA-248v-346w-9cwc) ], From 0b2367a18275e470735e18be2ff426f2509dd3e2 Mon Sep 17 00:00:00 2001 From: Arun Sureddi Date: Tue, 12 Nov 2024 13:47:07 -0800 Subject: [PATCH 03/12] Deprecate support for python 3.8 --- .github/workflows/common-server-ci.yml | 2 +- .github/workflows/inference-http-server-E2E-ci.yml | 2 +- .github/workflows/publish.yml | 2 +- README.md | 2 +- pyproject.toml | 2 +- setup.py | 3 +-- 6 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/common-server-ci.yml b/.github/workflows/common-server-ci.yml index 966f4e5..e6639bb 100644 --- a/.github/workflows/common-server-ci.yml +++ b/.github/workflows/common-server-ci.yml @@ -20,7 +20,7 @@ jobs: Server-Tests: strategy: matrix: - python_version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + python_version: ['3.9', '3.10', '3.11', '3.12'] pool_vmImage: ['ubuntu-latest', 'windows-latest'] runs-on: ${{matrix.pool_vmImage}} environment: Server-CI diff --git a/.github/workflows/inference-http-server-E2E-ci.yml b/.github/workflows/inference-http-server-E2E-ci.yml index 8455fd8..8964003 100644 --- a/.github/workflows/inference-http-server-E2E-ci.yml +++ b/.github/workflows/inference-http-server-E2E-ci.yml @@ -20,7 +20,7 @@ jobs: azureml-inference-http-server-E2E-CI: strategy: matrix: - python_version: ['3.8', '3.9', '3.10', '3.11', '3.12'] + python_version: ['3.9', '3.10', '3.11', '3.12'] pool_vmImage: ['ubuntu-latest', 'windows-latest'] runs-on: ${{matrix.pool_vmImage}} environment: Server-CI diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 609ac85..1b95726 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -15,7 +15,7 @@ jobs: - name: Setup python version uses: actions/setup-python@v4 with: - python-version: '3.8' + python-version: '3.9' - name: Build wheels run: | diff --git a/README.md b/README.md index 1b8866b..90018e5 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ This is the Flask server or the Sanic server code. The azureml-inference-server- ### Setting your environment - Clone the [azureml-inference-server](https://github.com/microsoft/azureml-inference-server) repository. -- Install [Python 3.8](https://www.python.org/downloads/). +- Install [Python 3.12](https://www.python.org/downloads/). - Install the virtualenv python package with `pip install virtualenv`. - Create a new virtual environment with `virtualenv `, for example `virtualenv amlinf`. - Activate the new environment. diff --git a/pyproject.toml b/pyproject.toml index 49faceb..8cc885f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ markers = [ [tool.black] line-length = 119 - target-version = ['py38'] + target-version = ['py39'] extend-exclude = ''' ^/( env/ diff --git a/setup.py b/setup.py index 3b25cdc..ca66fae 100644 --- a/setup.py +++ b/setup.py @@ -51,7 +51,6 @@ def get_license(): classifiers=[ "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", @@ -63,7 +62,7 @@ def get_license(): "Operating System :: MacOS", "Operating System :: POSIX :: Linux", ], - python_requires=">=3.8", + python_requires=">=3.9", install_requires=[ "flask<=2.3.2", # We aim to be compatible with both flask 1 and 2 "flask-cors~=5.0.0", From 7596e10eb5d97601cc66b7b8193699cd8c0812be Mon Sep 17 00:00:00 2001 From: Arun Sureddi Date: Tue, 12 Nov 2024 13:53:59 -0800 Subject: [PATCH 04/12] revert waitress version --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index de6633a..c4e9d3c 100644 --- a/setup.py +++ b/setup.py @@ -73,7 +73,7 @@ def get_license(): 'psutil<6.0.0; platform_system=="Windows"', "pydantic~=2.9.0", "pydantic-settings", - 'waitress>=3.0.1; platform_system=="Windows"', + 'waitress==2.1.2; platform_system=="Windows"', "werkzeug>=3.0.3", # Werkzeug 3.x breaks back-compatibility of urls package "certifi>=2024.7.4", # Python (Pip) Security Update for certifi (GHSA-248v-346w-9cwc) ], From 01dc5f2b144311389a465a4919a1bab12aeff71b Mon Sep 17 00:00:00 2001 From: Arun Sureddi Date: Tue, 12 Nov 2024 14:51:12 -0800 Subject: [PATCH 05/12] Clean up imports --- azureml_inference_server_http/server/create_app.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/azureml_inference_server_http/server/create_app.py b/azureml_inference_server_http/server/create_app.py index 3bcdb0c..1a354b8 100644 --- a/azureml_inference_server_http/server/create_app.py +++ b/azureml_inference_server_http/server/create_app.py @@ -1,22 +1,15 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -from distutils.version import LooseVersion -import functools import importlib import logging -import traceback -import warnings -import flask from flask import Flask -import werkzeug import werkzeug.datastructures from werkzeug.exceptions import HTTPException from azureml_inference_server_http.api.aml_response import AMLResponse from . import routes -from .config import config logger = logging.getLogger("azmlinfsrv") From d0d62048c79f1f38843f3748a689c5e2c3dc96b0 Mon Sep 17 00:00:00 2001 From: Arun Sureddi Date: Tue, 12 Nov 2024 15:11:00 -0800 Subject: [PATCH 06/12] Remove unused test --- .../server/create_app.py | 1 + tests/server/test_compat.py | 16 ---------------- 2 files changed, 1 insertion(+), 16 deletions(-) diff --git a/azureml_inference_server_http/server/create_app.py b/azureml_inference_server_http/server/create_app.py index 1a354b8..9e35265 100644 --- a/azureml_inference_server_http/server/create_app.py +++ b/azureml_inference_server_http/server/create_app.py @@ -13,6 +13,7 @@ logger = logging.getLogger("azmlinfsrv") + def create(): app = Flask(__name__) # To create a new instance of main_blueprint each time the create() is called diff --git a/tests/server/test_compat.py b/tests/server/test_compat.py index d6f1d3b..550e3f1 100644 --- a/tests/server/test_compat.py +++ b/tests/server/test_compat.py @@ -10,22 +10,6 @@ from .common import TestingClient -def test_compat_flask2_json(app: flask.Flask, client: TestingClient): - """Ensure that `request.json` does not throw in flask 2 when compatibility flag is set.""" - - @app.set_user_run - @rawhttp - def run(request: flask.Request): - # This property decodes the request data as JSON. Before flask 2 this property returns `None` when the request - # content-type is not json. Flask 2 modified the behavior to throw when content-type is not json. - return AMLResponse(str(request.json), 200) - - data = json.dumps({"a": 1}) - response = client.post_score(data=data) - assert response.status_code == 200 - assert response.data == b"None" - - def test_compat_flask2_json_error(app: flask.Flask, client: TestingClient): """Ensure that `request.json` correctly calls on_json_loading_failed() when it cannot parse the input JSON.""" From b71cf4441f3d94937aafe2dfcc8d0304466ae6db Mon Sep 17 00:00:00 2001 From: Arun Sureddi Date: Tue, 12 Nov 2024 15:33:16 -0800 Subject: [PATCH 07/12] Remove werkzeug datastructures --- azureml_inference_server_http/server/create_app.py | 1 - 1 file changed, 1 deletion(-) diff --git a/azureml_inference_server_http/server/create_app.py b/azureml_inference_server_http/server/create_app.py index 9e35265..f6f093e 100644 --- a/azureml_inference_server_http/server/create_app.py +++ b/azureml_inference_server_http/server/create_app.py @@ -5,7 +5,6 @@ import logging from flask import Flask -import werkzeug.datastructures from werkzeug.exceptions import HTTPException from azureml_inference_server_http.api.aml_response import AMLResponse From 1757aacbd3562c22a8ec67918c7c5a728800dd3e Mon Sep 17 00:00:00 2001 From: Arun Sureddi Date: Tue, 12 Nov 2024 17:00:48 -0800 Subject: [PATCH 08/12] Update test valid score.py --- tests/azmlinfsrv/resources/valid_score.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/azmlinfsrv/resources/valid_score.py b/tests/azmlinfsrv/resources/valid_score.py index fedf70c..2d3d246 100644 --- a/tests/azmlinfsrv/resources/valid_score.py +++ b/tests/azmlinfsrv/resources/valid_score.py @@ -38,9 +38,9 @@ def run(input_data): } r['pid'] = os.getpid() - if input_data.headers.has_key('sleep-in-sec'): - print(f'sleep-in-sec: {input_data.headers["sleep-in-sec"]}') - time.sleep(float(input_data.headers['sleep-in-sec'])) + if 'sleep-in-sec' in r['received_headers']: + print(f'sleep-in-sec: {r['received_headers']['sleep-in-sec']}') + time.sleep(float(r['received_headers']['sleep-in-sec'])) run_time = time.time() - start_time return AMLResponse(r, 200, {"pid": str(os.getpid()), "x-ms-run-function-duration": str(run_time)}, json_str=True) From cf5b233b491967976d8918dd7e5a480dfe665fa6 Mon Sep 17 00:00:00 2001 From: Arun Sureddi Date: Wed, 13 Nov 2024 11:46:20 -0800 Subject: [PATCH 09/12] fix reading from dictionary --- tests/azmlinfsrv/resources/valid_score.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/azmlinfsrv/resources/valid_score.py b/tests/azmlinfsrv/resources/valid_score.py index 2d3d246..8791b6a 100644 --- a/tests/azmlinfsrv/resources/valid_score.py +++ b/tests/azmlinfsrv/resources/valid_score.py @@ -39,8 +39,9 @@ def run(input_data): r['pid'] = os.getpid() if 'sleep-in-sec' in r['received_headers']: - print(f'sleep-in-sec: {r['received_headers']['sleep-in-sec']}') - time.sleep(float(r['received_headers']['sleep-in-sec'])) + sleep = r['received_headers']['sleep-in-sec'] + print(f'sleep-in-sec: {sleep}') + time.sleep(float(sleep)) run_time = time.time() - start_time return AMLResponse(r, 200, {"pid": str(os.getpid()), "x-ms-run-function-duration": str(run_time)}, json_str=True) From a3e8d1f5698a59c29e321e1f9d67686bcee641b8 Mon Sep 17 00:00:00 2001 From: Arun Sureddi Date: Wed, 13 Nov 2024 19:54:49 -0800 Subject: [PATCH 10/12] Resolve mdc store local field name with pydantic namespace --- azureml_inference_server_http/server/appinsights_client.py | 2 +- azureml_inference_server_http/server/config.py | 2 +- tests/server/conftest.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/azureml_inference_server_http/server/appinsights_client.py b/azureml_inference_server_http/server/appinsights_client.py index 4736484..972265e 100644 --- a/azureml_inference_server_http/server/appinsights_client.py +++ b/azureml_inference_server_http/server/appinsights_client.py @@ -72,7 +72,7 @@ def log_app_insights_exception(self, ex): def send_model_data_log(self, request_id, client_request_id, model_input, prediction): try: - if not self.enabled or not config.model_dc_storage_enabled: + if not self.enabled or not config.mdc_storage_enabled: return properties = { "custom_dimensions": { diff --git a/azureml_inference_server_http/server/config.py b/azureml_inference_server_http/server/config.py index bac004b..544b639 100644 --- a/azureml_inference_server_http/server/config.py +++ b/azureml_inference_server_http/server/config.py @@ -128,7 +128,7 @@ class AMLInferenceServerConfig(BaseSettings): app_insights_key: Optional[pydantic.SecretStr] = pydantic.Field(default=None) # Whether to enable model data collection - model_dc_storage_enabled: bool = pydantic.Field(default=False) + mdc_storage_enabled: bool = pydantic.Field(default=False) # Whether to log response to AppInsights app_insights_log_response_enabled: bool = pydantic.Field(default=True, alias="APP_INSIGHTS_LOG_RESPONSE_ENABLED") diff --git a/tests/server/conftest.py b/tests/server/conftest.py index 2bf4d36..2f52b2d 100644 --- a/tests/server/conftest.py +++ b/tests/server/conftest.py @@ -57,7 +57,7 @@ def app_cors(config): @pytest.fixture() def app_appinsights(config): config.app_insights_enabled = True - config.model_dc_storage_enabled = True + config.mdc_storage_enabled = True config.app_insights_key = pydantic.SecretStr(str(uuid.uuid4())) return create_app() From c40f0a925c41c20d36306815395adfe2288d89b0 Mon Sep 17 00:00:00 2001 From: Arun Sureddi Date: Wed, 13 Nov 2024 20:16:03 -0800 Subject: [PATCH 11/12] Update version --- azureml_inference_server_http/_version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azureml_inference_server_http/_version.py b/azureml_inference_server_http/_version.py index 0e8d75c..fc19b39 100644 --- a/azureml_inference_server_http/_version.py +++ b/azureml_inference_server_http/_version.py @@ -1,4 +1,4 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. -__version__ = "1.3.4" +__version__ = "1.4.0" From a4911a2f068ad97a18a2e52df9b3f7d39e73c014 Mon Sep 17 00:00:00 2001 From: Arun Sureddi Date: Wed, 13 Nov 2024 20:40:59 -0800 Subject: [PATCH 12/12] Updated changelog --- CHANGELOG.rst | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index a4f5d5b..267fe00 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,3 +1,31 @@ +1.4.0 (2024-11-13) +~~~~~~~~~~~~~~~~~~ +Azureml_Inference_Server_Http 1.4.0 (2024-11-13) +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Deprecation +----------- + +- Python 3.8 has reached the end-of-maintenance update support. The lifespan notification can be found + at https://peps.python.org/pep-0569/#lifespan. The Azureml_Inference_Server_Http dropped the support of Python 3.8 + to avoid patching unsupported Python 3.8 packages. + +- Deprecated the previous added support for Flask 2.0. A compatibility layer was introduced to ensure the flask 2.0 upgrade + doesn't break the users who use ``@rawhttp`` as the methods on the Flask request object. Specifically, + + * ``request.headers.has_keys()`` was removed + * ``request.json`` throws an exception if the content-type is not "application/json". Previously it returns ``None``. + + The compatibility layer which restored these functionalities to their previous behaviors is now removed. Users + are encouraged to audit their score scripts and migrate your score script to be compatible with Flask 2. + + Flask's full changelog can be found here: https://flask.palletsprojects.com/en/2.1.x/changes/ + +Enhancements +------------ + +- Upgraded waitress (only windows) package to 3.0.1 + 1.3.4 (2024-10-21) ~~~~~~~~~~~~~~~~~~ Azureml_Inference_Server_Http 1.3.4 (2024-10-21)