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."""