Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/common-server-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/inference-http-server-E2E-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down
28 changes: 28 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ This is the Flask server or the Sanic server code. The azureml-inference-server-
### <a name="virtualenv">Setting your environment</a>

- 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 <env name>`, for example `virtualenv amlinf`.
- Activate the new environment.
Expand Down
2 changes: 1 addition & 1 deletion azureml_inference_server_http/_version.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.

__version__ = "1.3.4"
__version__ = "1.4.0"
2 changes: 1 addition & 1 deletion azureml_inference_server_http/server/appinsights_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
6 changes: 1 addition & 5 deletions azureml_inference_server_http/server/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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")

Expand All @@ -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")
Expand Down
51 changes: 0 additions & 51 deletions azureml_inference_server_http/server/create_app.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down
1 change: 0 additions & 1 deletion docs/AzureMLInferenceServer.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ markers = [

[tool.black]
line-length = 119
target-version = ['py38']
target-version = ['py39']
extend-exclude = '''
^/(
env/
Expand Down
7 changes: 3 additions & 4 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -63,17 +62,17 @@ 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",
"opencensus-ext-azure~=1.1.0",
'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)
],
Expand Down
7 changes: 4 additions & 3 deletions tests/azmlinfsrv/resources/valid_score.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 1 addition & 1 deletion tests/server/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down
16 changes: 0 additions & 16 deletions tests/server/test_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""

Expand Down