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/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) 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/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" 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 8dc200f..544b639 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") @@ -132,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/azureml_inference_server_http/server/create_app.py b/azureml_inference_server_http/server/create_app.py index 5ac283b..f6f093e 100644 --- a/azureml_inference_server_http/server/create_app.py +++ b/azureml_inference_server_http/server/create_app.py @@ -1,69 +1,18 @@ # 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") -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/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..563afb5 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,9 +62,9 @@ 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~=3.0.0", "flask-cors~=5.0.0", 'gunicorn>=23.0.0; platform_system!="Windows"', "inference-schema~=1.8.0", @@ -73,7 +72,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) ], diff --git a/tests/azmlinfsrv/resources/valid_score.py b/tests/azmlinfsrv/resources/valid_score.py index fedf70c..8791b6a 100644 --- a/tests/azmlinfsrv/resources/valid_score.py +++ b/tests/azmlinfsrv/resources/valid_score.py @@ -38,9 +38,10 @@ 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']: + 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) 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() 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."""