From efc02f3a4643c6313b6e59d8abf27fd10133b6fe Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Thu, 9 Nov 2023 14:41:23 -0500
Subject: [PATCH 001/101] Http Proxy Support
Http Proxy Support
integration
refactor
update ext name
add to env reload
add dep
revert
---
azure_functions_worker/bindings/meta.py | 36 +-
azure_functions_worker/constants.py | 13 +
azure_functions_worker/dispatcher.py | 227 +++++++---
azure_functions_worker/functions.py | 18 +-
azure_functions_worker/http_proxy.py | 148 +++++++
azure_functions_worker/loader.py | 53 +--
azure_functions_worker/logging.py | 3 +-
python/test/worker.config.json | 2 +-
setup.py | 5 +-
.../fastapi_data_class/function_app.py | 108 +++++
.../function_app.py | 108 +++++
.../function_app.py | 108 +++++
.../function_app.py | 108 +++++
.../http_v2_functions/function_app.py | 419 ++++++++++++++++++
tests/unittests/test_http_functions.py | 7 +-
15 files changed, 1265 insertions(+), 98 deletions(-)
create mode 100644 azure_functions_worker/http_proxy.py
create mode 100644 tests/endtoend/http_v2_functions/fastapi_data_class/function_app.py
create mode 100644 tests/endtoend/http_v2_functions/fastapi_non_streaming_custom_resps/function_app.py
create mode 100644 tests/endtoend/http_v2_functions/fastapi_streaming_download_func/function_app.py
create mode 100644 tests/endtoend/http_v2_functions/fastapi_streaming_upload_func/function_app.py
create mode 100644 tests/unittests/http_functions/http_v2_functions/function_app.py
diff --git a/azure_functions_worker/bindings/meta.py b/azure_functions_worker/bindings/meta.py
index f7a810145..ccfaa8200 100644
--- a/azure_functions_worker/bindings/meta.py
+++ b/azure_functions_worker/bindings/meta.py
@@ -3,8 +3,9 @@
import sys
import typing
-from .. import protos
+from azure_functions_worker.constants import HTTP, HTTP_TRIGGER
+from .. import protos
from . import datumdef
from . import generic
from .shared_memory_data_transfer import SharedMemoryManager
@@ -14,6 +15,30 @@
PB_TYPE_RPC_SHARED_MEMORY = 'rpc_shared_memory'
BINDING_REGISTRY = None
+def _check_http_input_type_annotation(bind_name: str, pytype: type) -> bool:
+ ext_base = sys.modules.get('azure.functions.extension.base')
+ if ext_base is not None and ext_base.HttpV2FeatureChecker.http_v2_enabled():
+ return ext_base.RequestTrackerMeta.check_type(pytype)
+
+ binding = get_binding(bind_name)
+ return binding.check_input_type_annotation(pytype)
+
+def _check_http_output_type_annotation(bind_name: str, pytype: type) -> bool:
+ ext_base = sys.modules.get('azure.functions.extension.base')
+ if ext_base is not None and ext_base.HttpV2FeatureChecker.http_v2_enabled():
+ return ext_base.ResponseTrackerMeta.check_type(pytype)
+
+ binding = get_binding(bind_name)
+ return binding.check_output_type_annotation(pytype)
+
+
+INPUT_TYPE_CHECK_OVERRIDE_MAP = {
+ HTTP_TRIGGER: _check_http_input_type_annotation
+}
+
+OUTPUT_TYPE_CHECK_OVERRIDE_MAP = {
+ HTTP: _check_http_output_type_annotation
+}
def load_binding_registry() -> None:
func = sys.modules.get('azure.functions')
@@ -43,11 +68,18 @@ def is_trigger_binding(bind_name: str) -> bool:
def check_input_type_annotation(bind_name: str, pytype: type) -> bool:
+ global INPUT_TYPE_CHECK_OVERRIDE_MAP
+ if bind_name in INPUT_TYPE_CHECK_OVERRIDE_MAP:
+ return INPUT_TYPE_CHECK_OVERRIDE_MAP[bind_name](bind_name, pytype)
+
binding = get_binding(bind_name)
return binding.check_input_type_annotation(pytype)
-
def check_output_type_annotation(bind_name: str, pytype: type) -> bool:
+ global OUTPUT_TYPE_CHECK_OVERRIDE_MAP
+ if bind_name in OUTPUT_TYPE_CHECK_OVERRIDE_MAP:
+ return OUTPUT_TYPE_CHECK_OVERRIDE_MAP[bind_name](bind_name, pytype)
+
binding = get_binding(bind_name)
return binding.check_output_type_annotation(pytype)
diff --git a/azure_functions_worker/constants.py b/azure_functions_worker/constants.py
index b6cc668b6..7f09d586c 100644
--- a/azure_functions_worker/constants.py
+++ b/azure_functions_worker/constants.py
@@ -10,6 +10,7 @@
WORKER_STATUS = "WorkerStatus"
SHARED_MEMORY_DATA_TRANSFER = "SharedMemoryDataTransfer"
FUNCTION_DATA_CACHE = "FunctionDataCache"
+HTTP_URI = "HttpUri"
# Platform Environment Variables
AZURE_WEBJOBS_SCRIPT_ROOT = "AzureWebJobsScriptRoot"
@@ -60,3 +61,15 @@
PYTHON_ENABLE_INIT_INDEXING = "PYTHON_ENABLE_INIT_INDEXING"
METADATA_PROPERTIES_WORKER_INDEXED = "worker_indexed"
+
+# HostNames
+LOCAL_HOST = "localhost"
+
+# Header names
+X_MS_INVOCATION_ID = "x-ms-invocation-id"
+
+# Trigger Names
+HTTP_TRIGGER = "httpTrigger"
+
+# Output Names
+HTTP = "http"
diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py
index 2f94e21ba..35143fb68 100644
--- a/azure_functions_worker/dispatcher.py
+++ b/azure_functions_worker/dispatcher.py
@@ -7,6 +7,7 @@
import asyncio
import concurrent.futures
+import importlib
import logging
import os
import platform
@@ -19,10 +20,10 @@
from datetime import datetime
import grpc
-
+import socket
from . import bindings, constants, functions, loader, protos
from .bindings.shared_memory_data_transfer import SharedMemoryManager
-from .constants import (PYTHON_ROLLBACK_CWD_PATH,
+from .constants import (HTTP_TRIGGER, PYTHON_ROLLBACK_CWD_PATH,
PYTHON_THREADPOOL_THREAD_COUNT,
PYTHON_THREADPOOL_THREAD_COUNT_DEFAULT,
PYTHON_THREADPOOL_THREAD_COUNT_MAX_37,
@@ -31,8 +32,10 @@
PYTHON_SCRIPT_FILE_NAME,
PYTHON_SCRIPT_FILE_NAME_DEFAULT,
PYTHON_LANGUAGE_RUNTIME, PYTHON_ENABLE_INIT_INDEXING,
+ X_MS_INVOCATION_ID, LOCAL_HOST,
METADATA_PROPERTIES_WORKER_INDEXED)
from .extension import ExtensionManager
+from .http_proxy import http_coordinator
from .logging import disable_console_logging, enable_console_logging
from .logging import (logger, error_logger, is_system_log_category,
CONSOLE_LOG_PREFIX, format_exception)
@@ -58,6 +61,19 @@ def current(mcls):
return disp
+def get_unused_tcp_port():
+ # Create a TCP socket
+ tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ # Bind it to a free port provided by the OS
+ tcp_socket.bind(("", 0))
+ # Get the port number
+ port = tcp_socket.getsockname()[1]
+ # Close the socket
+ tcp_socket.close()
+ # Return the port number
+ return port
+
+
class Dispatcher(metaclass=DispatcherMeta):
_GRPC_STOP_RESPONSE = object()
@@ -74,6 +90,7 @@ def __init__(self, loop: BaseEventLoop, host: str, port: int,
self._functions = functions.Registry()
self._shmem_mgr = SharedMemoryManager()
self._old_task_factory = None
+ self.function_metadata_result = None
# Used to store metadata returns
self._function_metadata_result = None
@@ -128,7 +145,7 @@ async def connect(cls, host: str, port: int, worker_id: str,
async def dispatch_forever(self): # sourcery skip: swap-if-expression
if DispatcherMeta.__current_dispatcher__ is not None:
raise RuntimeError('there can be only one running dispatcher per '
- 'process')
+ 'process')
self._old_task_factory = self._loop.get_task_factory()
@@ -156,8 +173,10 @@ async def dispatch_forever(self): # sourcery skip: swap-if-expression
logging_handler = AsyncLoggingHandler()
root_logger = logging.getLogger()
+
log_level = logging.INFO if not is_envvar_true(
PYTHON_ENABLE_DEBUG_LOGGING) else logging.DEBUG
+
root_logger.setLevel(log_level)
root_logger.addHandler(logging_handler)
logger.info('Switched to gRPC logging.')
@@ -263,59 +282,77 @@ async def _dispatch_grpc_request(self, request):
self._grpc_resp_queue.put_nowait(resp)
async def _handle__worker_init_request(self, request):
- logger.info('Received WorkerInitRequest, '
- 'python version %s, '
- 'worker version %s, '
- 'request ID %s. '
- 'App Settings state: %s. '
- 'To enable debug level logging, please refer to '
- 'https://aka.ms/python-enable-debug-logging',
- sys.version,
- VERSION,
- self.request_id,
- get_python_appsetting_state()
- )
-
- worker_init_request = request.worker_init_request
- host_capabilities = worker_init_request.capabilities
- if constants.FUNCTION_DATA_CACHE in host_capabilities:
- val = host_capabilities[constants.FUNCTION_DATA_CACHE]
- self._function_data_cache_enabled = val == _TRUE
-
- capabilities = {
- constants.RAW_HTTP_BODY_BYTES: _TRUE,
- constants.TYPED_DATA_COLLECTION: _TRUE,
- constants.RPC_HTTP_BODY_ONLY: _TRUE,
- constants.WORKER_STATUS: _TRUE,
- constants.RPC_HTTP_TRIGGER_METADATA_REMOVED: _TRUE,
- constants.SHARED_MEMORY_DATA_TRANSFER: _TRUE,
- }
-
- if DependencyManager.should_load_cx_dependencies():
- DependencyManager.prioritize_customer_dependencies()
-
- if DependencyManager.is_in_linux_consumption():
- import azure.functions # NoQA
-
- # loading bindings registry and saving results to a static
- # dictionary which will be later used in the invocation request
- bindings.load_binding_registry()
-
- if is_envvar_true(PYTHON_ENABLE_INIT_INDEXING):
- try:
- self.load_function_metadata(
- worker_init_request.function_app_directory,
- caller_info="worker_init_request")
- except Exception as ex:
- self._function_metadata_exception = ex
+ try:
+ logger.info('Received WorkerInitRequest, '
+ 'python version %s, '
+ 'worker version %s, '
+ 'request ID %s. '
+ 'App Settings state: %s. '
+ 'To enable debug level logging, please refer to '
+ 'https://aka.ms/python-enable-debug-logging',
+ sys.version,
+ VERSION,
+ self.request_id,
+ get_python_appsetting_state()
+ )
+
+ worker_init_request = request.worker_init_request
+ directory = worker_init_request.function_app_directory
+ host_capabilities = worker_init_request.capabilities
+ if constants.FUNCTION_DATA_CACHE in host_capabilities:
+ val = host_capabilities[constants.FUNCTION_DATA_CACHE]
+ self._function_data_cache_enabled = val == _TRUE
+
+ capabilities = {
+ constants.RAW_HTTP_BODY_BYTES: _TRUE,
+ constants.TYPED_DATA_COLLECTION: _TRUE,
+ constants.RPC_HTTP_BODY_ONLY: _TRUE,
+ constants.WORKER_STATUS: _TRUE,
+ constants.RPC_HTTP_TRIGGER_METADATA_REMOVED: _TRUE,
+ constants.SHARED_MEMORY_DATA_TRANSFER: _TRUE,
+ }
+
+ if DependencyManager.should_load_cx_dependencies():
+ DependencyManager.prioritize_customer_dependencies()
+
+ if DependencyManager.is_in_linux_consumption():
+ import azure.functions # NoQA
+
+ # loading bindings registry and saving results to a static
+ # dictionary which will be later used in the invocation request
+ bindings.load_binding_registry()
- return protos.StreamingMessage(
- request_id=self.request_id,
- worker_init_response=protos.WorkerInitResponse(
- capabilities=capabilities,
- worker_metadata=self.get_worker_metadata(),
- result=protos.StatusResult(
- status=protos.StatusResult.Success)))
+ if is_envvar_true(PYTHON_ENABLE_INIT_INDEXING):
+ try:
+ self.load_function_metadata(
+ worker_init_request.function_app_directory,
+ caller_info="worker_init_request")
+ except Exception as ex:
+ self._function_metadata_exception = ex
+
+ if self._has_http_func:
+ from azure.functions.extension.base import HttpV2FeatureChecker
+
+ if HttpV2FeatureChecker.http_v2_enabled():
+ capabilities[constants.HTTP_URI] = await self._initialize_http_server()
+
+ return protos.StreamingMessage(
+ request_id=self.request_id,
+ worker_init_response=protos.WorkerInitResponse(
+ capabilities=capabilities,
+ worker_metadata=self.get_worker_metadata(),
+ result=protos.StatusResult(status=protos.StatusResult.Success),
+ ),
+ )
+ except Exception as e:
+ logger.error("Error handling WorkerInitRequest: %s", str(e))
+ return protos.StreamingMessage(
+ request_id=self.request_id,
+ worker_init_response=protos.WorkerInitResponse(
+ result=protos.StatusResult(status=protos.StatusResult.Failure,
+ exception=self._serialize_exception(e))
+ ),
+ )
async def _handle__worker_status_request(self, request):
# Logging is not necessary in this request since the response is used
@@ -348,6 +385,7 @@ def load_function_metadata(self, function_app_directory, caller_info):
self.index_functions(function_path)) \
if os.path.exists(function_path) else None
+
async def _handle__functions_metadata_request(self, request):
metadata_request = request.functions_metadata_request
function_app_directory = metadata_request.function_app_directory
@@ -466,6 +504,7 @@ async def _handle__function_load_request(self, request):
status=protos.StatusResult.Success)))
except Exception as ex:
+ logging.error(ex)
return protos.StreamingMessage(
request_id=self.request_id,
function_load_response=protos.FunctionLoadResponse(
@@ -508,19 +547,28 @@ async def _handle__invocation_request(self, request):
logger.info(', '.join(function_invocation_logs))
args = {}
+
for pb in invoc_request.input_data:
pb_type_info = fi.input_types[pb.name]
+ trigger_metadata = None
if bindings.is_trigger_binding(pb_type_info.binding_name):
trigger_metadata = invoc_request.trigger_metadata
- else:
- trigger_metadata = None
-
+
args[pb.name] = bindings.from_incoming_proto(
pb_type_info.binding_name, pb,
trigger_metadata=trigger_metadata,
pytype=pb_type_info.pytype,
shmem_mgr=self._shmem_mgr)
+ if fi.trigger_metadata.get('type') == HTTP_TRIGGER:
+ from azure.functions.extension.base import HttpV2FeatureChecker
+ http_v2_enabled = HttpV2FeatureChecker.http_v2_enabled()
+
+ if http_v2_enabled:
+ http_request = await http_coordinator.get_http_request_async(
+ invocation_id)
+ args[fi.trigger_metadata.get('param_name')] = http_request
+
fi_context = self._get_context(invoc_request, fi.name, fi.directory)
# Use local thread storage to store the invocation ID
@@ -537,14 +585,19 @@ async def _handle__invocation_request(self, request):
call_result = await self._run_async_func(
fi_context, fi.func, args
)
+
else:
call_result = await self._loop.run_in_executor(
self._sync_call_tp,
self._run_sync_func,
invocation_id, fi_context, fi.func, args)
+
if call_result is not None and not fi.has_return:
raise RuntimeError(f'function {fi.name!r} without a $return '
'binding returned a non-None value')
+
+ if http_v2_enabled:
+ http_coordinator.set_http_response(invocation_id, call_result)
output_data = []
cache_enabled = self._function_data_cache_enabled
@@ -564,10 +617,12 @@ async def _handle__invocation_request(self, request):
output_data.append(param_binding)
return_value = None
- if fi.return_type is not None:
+ if fi.return_type is not None and not http_v2_enabled:
return_value = bindings.to_outgoing_proto(
- fi.return_type.binding_name, call_result,
- pytype=fi.return_type.pytype)
+ fi.return_type.binding_name,
+ call_result,
+ pytype=fi.return_type.pytype,
+ )
# Actively flush customer print() function to console
sys.stdout.flush()
@@ -590,6 +645,7 @@ async def _handle__invocation_request(self, request):
status=protos.StatusResult.Failure,
exception=self._serialize_exception(ex))))
+
async def _handle__function_environment_reload_request(self, request):
"""Only runs on Linux Consumption placeholder specialization.
This is called only when placeholder mode is true. On worker restarts
@@ -638,6 +694,7 @@ async def _handle__function_environment_reload_request(self, request):
# reload_customer_libraries call clears the registry
bindings.load_binding_registry()
+ capabilities = {}
if is_envvar_true(PYTHON_ENABLE_INIT_INDEXING):
try:
self.load_function_metadata(
@@ -645,6 +702,12 @@ async def _handle__function_environment_reload_request(self, request):
caller_info="environment_reload_request")
except Exception as ex:
self._function_metadata_exception = ex
+
+ if self._has_http_func:
+ from azure.functions.extension.base import HttpV2FeatureChecker
+
+ if HttpV2FeatureChecker.http_v2_enabled():
+ capabilities[constants.HTTP_URI] = await self._initialize_http_server()
# Change function app directory
if getattr(func_env_reload_request,
@@ -653,7 +716,7 @@ async def _handle__function_environment_reload_request(self, request):
func_env_reload_request.function_app_directory)
success_response = protos.FunctionEnvironmentReloadResponse(
- capabilities={},
+ capabilities=capabilities,
worker_metadata=self.get_worker_metadata(),
result=protos.StatusResult(
status=protos.StatusResult.Success))
@@ -672,10 +735,43 @@ async def _handle__function_environment_reload_request(self, request):
request_id=self.request_id,
function_environment_reload_response=failure_response)
+ async def _initialize_http_server(self):
+ from azure.functions.extension.base import ModuleTrackerMeta, RequestTrackerMeta
+
+ web_extension_mod_name = ModuleTrackerMeta.get_module()
+ extension_module = importlib.import_module(web_extension_mod_name)
+ web_app_class = extension_module.WebApp
+ web_server_class = extension_module.WebServer
+
+ unused_port = get_unused_tcp_port()
+
+ app = web_app_class()
+ request_type = RequestTrackerMeta.get_request_type()
+
+ @app.route
+ async def catch_all(request: request_type): # type: ignore
+ invoc_id = request.headers.get(X_MS_INVOCATION_ID)
+ if invoc_id is None:
+ raise ValueError(f"Header {X_MS_INVOCATION_ID} not found")
+
+ http_coordinator.set_http_request(invoc_id, request)
+ http_resp = await http_coordinator.await_http_response_async(invoc_id)
+ return http_resp
+
+ web_server = web_server_class(LOCAL_HOST, unused_port, app)
+ web_server_run_task = web_server.serve()
+
+ loop = asyncio.get_event_loop()
+ loop.create_task(web_server_run_task)
+
+ return f"http://{LOCAL_HOST}:{unused_port}"
+
def index_functions(self, function_path: str):
indexed_functions = loader.index_function_app(function_path)
- logger.info('Indexed function app and found %s functions',
- len(indexed_functions))
+ logger.info(
+ "Indexed function app and found %s functions",
+ len(indexed_functions)
+ )
if indexed_functions:
fx_metadata_results = loader.process_indexed_function(
@@ -683,7 +779,9 @@ def index_functions(self, function_path: str):
indexed_functions)
indexed_function_logs: List[str] = []
+ self._has_http_func = False
for func in indexed_functions:
+ self._has_http_func = self._has_http_func or func.is_http_function()
function_log = "Function Name: {}, Function Binding: {}" \
.format(func.get_function_name(),
[(binding.type, binding.name) for binding in
@@ -876,7 +974,6 @@ def gen(resp_queue):
class AsyncLoggingHandler(logging.Handler):
-
def emit(self, record: LogRecord) -> None:
# Since we disable console log after gRPC channel is initiated,
# we should redirect all the messages into dispatcher.
diff --git a/azure_functions_worker/functions.py b/azure_functions_worker/functions.py
index f0926230c..0200c01ae 100644
--- a/azure_functions_worker/functions.py
+++ b/azure_functions_worker/functions.py
@@ -6,6 +6,8 @@
import typing
import uuid
+from azure_functions_worker.constants import HTTP_TRIGGER
+
from . import bindings as bindings_utils
from . import protos
from ._thirdparty import typing_inspect
@@ -31,6 +33,7 @@ class FunctionInfo(typing.NamedTuple):
output_types: typing.Mapping[str, ParamTypeInfo]
return_type: typing.Optional[ParamTypeInfo]
+ trigger_metadata: typing.Dict[str, typing.Any]
class FunctionLoadError(RuntimeError):
@@ -297,6 +300,17 @@ def add_func_to_registry_and_return_funcinfo(self, function,
str, ParamTypeInfo],
return_type: str):
+ http_trigger_param_name = next(
+ (input_type for input_type, type_info in input_types.items() if type_info.binding_name == HTTP_TRIGGER),
+ None
+ )
+
+ if http_trigger_param_name is not None:
+ trigger_metadata = {
+ "type": HTTP_TRIGGER,
+ "param_name": http_trigger_param_name
+ }
+
function_info = FunctionInfo(
func=function,
name=function_name,
@@ -307,7 +321,9 @@ def add_func_to_registry_and_return_funcinfo(self, function,
has_return=has_explicit_return or has_implicit_return,
input_types=input_types,
output_types=output_types,
- return_type=return_type)
+ return_type=return_type,
+ trigger_metadata=trigger_metadata)
+
self._functions[function_id] = function_info
return function_info
diff --git a/azure_functions_worker/http_proxy.py b/azure_functions_worker/http_proxy.py
new file mode 100644
index 000000000..eff55067e
--- /dev/null
+++ b/azure_functions_worker/http_proxy.py
@@ -0,0 +1,148 @@
+import abc
+import asyncio
+from typing import Dict
+
+class BaseContextReference(abc.ABC):
+ def __init__(self, event_class, http_request=None, http_response=None, function=None, fi_context=None, args=None, http_trigger_param_name=None):
+ self._http_request = http_request
+ self._http_response = http_response
+ self._function = function
+ self._fi_context = fi_context
+ self._args = args
+ self._http_trigger_param_name = http_trigger_param_name
+ self._http_request_available_event = event_class()
+ self._http_response_available_event = event_class()
+ self._rpc_invocation_ready_event = event_class()
+
+ @property
+ def http_request(self):
+ return self._http_request
+
+ @http_request.setter
+ def http_request(self, value):
+ self._http_request = value
+ self._http_request_available_event.set()
+
+ @property
+ def http_response(self):
+ return self._http_response
+
+ @http_response.setter
+ def http_response(self, value):
+ self._http_response = value
+ self._http_response_available_event.set()
+
+ @property
+ def function(self):
+ return self._function
+
+ @function.setter
+ def function(self, value):
+ self._function = value
+
+ @property
+ def fi_context(self):
+ return self._fi_context
+
+ @fi_context.setter
+ def fi_context(self, value):
+ self._fi_context = value
+
+ @property
+ def http_trigger_param_name(self):
+ return self._http_trigger_param_name
+
+ @http_trigger_param_name.setter
+ def http_trigger_param_name(self, value):
+ self._http_trigger_param_name = value
+
+ @property
+ def args(self):
+ return self._args
+
+ @args.setter
+ def args(self, value):
+ self._args = value
+
+ @property
+ def http_request_available_event(self):
+ return self._http_request_available_event
+
+ @property
+ def http_response_available_event(self):
+ return self._http_response_available_event
+
+ @property
+ def rpc_invocation_ready_event(self):
+ return self._rpc_invocation_ready_event
+
+
+class AsyncContextReference(BaseContextReference):
+ def __init__(self, http_request=None, http_response=None, function=None, fi_context=None, args=None):
+ super().__init__(event_class=asyncio.Event, http_request=http_request, http_response=http_response,
+ function=function, fi_context=fi_context, args=args)
+ self.is_async = True
+
+
+class SingletonMeta(type):
+ _instances = {}
+
+ def __call__(cls, *args, **kwargs):
+ if cls not in cls._instances:
+ cls._instances[cls] = super().__call__(*args, **kwargs)
+ return cls._instances[cls]
+
+
+class HttpCoordinator(metaclass=SingletonMeta):
+ def __init__(self):
+ self._context_references: Dict[str, BaseContextReference] = {}
+
+ def set_http_request(self, invoc_id, http_request):
+ if invoc_id not in self._context_references:
+ self._context_references[invoc_id] = AsyncContextReference()
+ context_ref = self._context_references.get(invoc_id)
+ context_ref.http_request = http_request
+ context_ref.http_request_available_event.set()
+
+ def set_http_response(self, invoc_id, http_response):
+ if invoc_id not in self._context_references:
+ raise Exception("No context reference found for invocation %s", invoc_id)
+ context_ref = self._context_references.get(invoc_id)
+ context_ref.http_response = http_response
+ context_ref.http_response_available_event.set()
+
+ async def get_http_request_async(self, invoc_id):
+ if invoc_id not in self._context_references:
+ self._context_references[invoc_id] = AsyncContextReference()
+
+ await asyncio.sleep(0)
+ await self._context_references.get(invoc_id).http_request_available_event.wait()
+ return self._pop_http_request(invoc_id)
+
+ async def await_http_response_async(self, invoc_id):
+ if invoc_id not in self._context_references:
+ raise Exception("No context reference found for invocation %s", invoc_id)
+ await asyncio.sleep(0)
+ await self._context_references.get(invoc_id).http_response_available_event.wait()
+ return self._pop_http_response(invoc_id)
+
+ def _pop_http_request(self, invoc_id):
+ context_ref = self._context_references.get(invoc_id)
+ request = context_ref.http_request
+ if request is not None:
+ context_ref.http_request = None
+ return request
+
+ raise Exception("No http request found for invocation %s", invoc_id)
+
+ def _pop_http_response(self, invoc_id):
+ context_ref = self._context_references.get(invoc_id)
+ response = context_ref.http_response
+ if response is not None:
+ context_ref.http_response = None
+ return response
+
+ raise Exception("No http response found for invocation %s", invoc_id)
+
+
+http_coordinator = HttpCoordinator()
diff --git a/azure_functions_worker/loader.py b/azure_functions_worker/loader.py
index 938fde64c..da706fffd 100644
--- a/azure_functions_worker/loader.py
+++ b/azure_functions_worker/loader.py
@@ -122,31 +122,34 @@ def build_variable_interval_retry(retry, max_retry_count, retry_strategy):
def process_indexed_function(functions_registry: functions.Registry,
indexed_functions):
- fx_metadata_results = []
- for indexed_function in indexed_functions:
- function_info = functions_registry.add_indexed_function(
- function=indexed_function)
-
- binding_protos = build_binding_protos(indexed_function)
- retry_protos = build_retry_protos(indexed_function)
-
- function_metadata = protos.RpcFunctionMetadata(
- name=function_info.name,
- function_id=function_info.function_id,
- managed_dependency_enabled=False, # only enabled for PowerShell
- directory=function_info.directory,
- script_file=indexed_function.function_script_file,
- entry_point=function_info.name,
- is_proxy=False, # not supported in V4
- language=PYTHON_LANGUAGE_RUNTIME,
- bindings=binding_protos,
- raw_bindings=indexed_function.get_raw_bindings(),
- retry_options=retry_protos,
- properties={METADATA_PROPERTIES_WORKER_INDEXED: "True"})
-
- fx_metadata_results.append(function_metadata)
-
- return fx_metadata_results
+ try:
+ fx_metadata_results = []
+ for indexed_function in indexed_functions:
+ function_info = functions_registry.add_indexed_function(
+ function=indexed_function)
+
+ binding_protos = build_binding_protos(indexed_function)
+ retry_protos = build_retry_protos(indexed_function)
+
+ function_metadata = protos.RpcFunctionMetadata(
+ name=function_info.name,
+ function_id=function_info.function_id,
+ managed_dependency_enabled=False, # only enabled for PowerShell
+ directory=function_info.directory,
+ script_file=indexed_function.function_script_file,
+ entry_point=function_info.name,
+ is_proxy=False, # not supported in V4
+ language=PYTHON_LANGUAGE_RUNTIME,
+ bindings=binding_protos,
+ raw_bindings=indexed_function.get_raw_bindings(),
+ retry_options=retry_protos,
+ properties={METADATA_PROPERTIES_WORKER_INDEXED: "True"})
+
+ fx_metadata_results.append(function_metadata)
+ return fx_metadata_results
+ except Exception as e:
+ logger.error(f'Error in process_indexed_function. {e}', exc_info=True)
+ raise e
@attach_message_to_exception(
diff --git a/azure_functions_worker/logging.py b/azure_functions_worker/logging.py
index adb5ff294..7a5340919 100644
--- a/azure_functions_worker/logging.py
+++ b/azure_functions_worker/logging.py
@@ -20,7 +20,8 @@
handler: Optional[logging.Handler] = None
error_handler: Optional[logging.Handler] = None
-
+local_handler = logging.FileHandler("E:/projects/AzureFunctionsPythonWorker/log.txt")
+logger.addHandler(local_handler)
def format_exception(exception: Exception) -> str:
msg = str(exception) + "\n"
diff --git a/python/test/worker.config.json b/python/test/worker.config.json
index 3fc2a9236..05f6d26ed 100644
--- a/python/test/worker.config.json
+++ b/python/test/worker.config.json
@@ -2,7 +2,7 @@
"description":{
"language":"python",
"extensions":[".py"],
- "defaultExecutablePath":"python",
+ "defaultExecutablePath":"E:\\projects\\AzureFunctionsPythonWorker\\.venv_3.8\\Scripts\\python.exe",
"defaultWorkerPath":"worker.py",
"workerIndexing": "true"
}
diff --git a/setup.py b/setup.py
index a3970fe19..f85f1c332 100644
--- a/setup.py
+++ b/setup.py
@@ -79,7 +79,7 @@
)
else:
INSTALL_REQUIRES.extend(
- ("protobuf~=4.22.0", "grpcio-tools~=1.54.2", "grpcio~=1.54.2")
+ ("protobuf~=4.22.0", "grpcio-tools~=1.54.2", "grpcio~=1.54.2", "azure-functions-extension-base")
)
EXTRA_REQUIRES = {
@@ -109,7 +109,8 @@
"pandas",
"numpy",
"pre-commit"
- ]
+ ],
+ "fastapi": ["azure-functions-extension-fastapi"]
}
diff --git a/tests/endtoend/http_v2_functions/fastapi_data_class/function_app.py b/tests/endtoend/http_v2_functions/fastapi_data_class/function_app.py
new file mode 100644
index 000000000..25900aafb
--- /dev/null
+++ b/tests/endtoend/http_v2_functions/fastapi_data_class/function_app.py
@@ -0,0 +1,108 @@
+import json
+import os
+import typing
+
+from azure.eventhub import EventData
+from azure.eventhub.aio import EventHubProducerClient
+
+import azure.functions as func
+
+app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
+
+
+# An HttpTrigger to generating EventHub event from EventHub Output Binding
+@app.function_name(name="eventhub_output")
+@app.route(route="eventhub_output")
+@app.event_hub_output(arg_name="event",
+ event_hub_name="python-worker-ci-eventhub-one",
+ connection="AzureWebJobsEventHubConnectionString")
+def eventhub_output(req: func.HttpRequest, event: func.Out[str]):
+ event.set(req.get_body().decode('utf-8'))
+ return 'OK'
+
+
+# This is an actual EventHub trigger which will convert the event data
+# into a storage blob.
+@app.function_name(name="eventhub_trigger")
+@app.event_hub_message_trigger(arg_name="event",
+ event_hub_name="python-worker-ci-eventhub-one",
+ connection="AzureWebJobsEventHubConnectionString"
+ )
+@app.blob_output(arg_name="$return",
+ path="python-worker-tests/test-eventhub-triggered.txt",
+ connection="AzureWebJobsStorage")
+def eventhub_trigger(event: func.EventHubEvent) -> bytes:
+ return event.get_body()
+
+
+# Retrieve the event data from storage blob and return it as Http response
+@app.function_name(name="get_eventhub_triggered")
+@app.route(route="get_eventhub_triggered")
+@app.blob_input(arg_name="file",
+ path="python-worker-tests/test-eventhub-triggered.txt",
+ connection="AzureWebJobsStorage")
+def get_eventhub_triggered(req: func.HttpRequest,
+ file: func.InputStream) -> str:
+ return file.read().decode('utf-8')
+
+
+# Retrieve the event data from storage blob and return it as Http response
+@app.function_name(name="get_metadata_triggered")
+@app.route(route="get_metadata_triggered")
+@app.blob_input(arg_name="file",
+ path="python-worker-tests/test-metadata-triggered.txt",
+ connection="AzureWebJobsStorage")
+async def get_metadata_triggered(req: func.HttpRequest,
+ file: func.InputStream) -> str:
+ return func.HttpResponse(body=file.read().decode('utf-8'),
+ status_code=200,
+ mimetype='application/json')
+
+
+# An HttpTrigger to generating EventHub event from azure-eventhub SDK.
+# Events generated from azure-eventhub contain the full metadata.
+@app.function_name(name="metadata_output")
+@app.route(route="metadata_output")
+async def metadata_output(req: func.HttpRequest):
+ # Parse event metadata from http request
+ json_string = req.get_body().decode('utf-8')
+ event_dict = json.loads(json_string)
+
+ # Create an EventHub Client and event batch
+ client = EventHubProducerClient.from_connection_string(
+ os.getenv('AzureWebJobsEventHubConnectionString'),
+ eventhub_name='python-worker-ci-eventhub-one-metadata')
+
+ # Generate new event based on http request with full metadata
+ event_data_batch = await client.create_batch()
+ event_data_batch.add(EventData(event_dict.get('body')))
+
+ # Send out event into event hub
+ try:
+ await client.send_batch(event_data_batch)
+ finally:
+ await client.close()
+
+ return 'OK'
+
+
+@app.function_name(name="metadata_trigger")
+@app.event_hub_message_trigger(
+ arg_name="event",
+ event_hub_name="python-worker-ci-eventhub-one-metadata",
+ connection="AzureWebJobsEventHubConnectionString")
+@app.blob_output(arg_name="$return",
+ path="python-worker-tests/test-metadata-triggered.txt",
+ connection="AzureWebJobsStorage")
+async def metadata_trigger(event: func.EventHubEvent) -> bytes:
+ event_dict: typing.Mapping[str, typing.Any] = {
+ 'body': event.get_body().decode('utf-8'),
+ # Uncomment this when the EnqueuedTimeUtc is fixed in azure-functions
+ # 'enqueued_time': event.enqueued_time.isoformat(),
+ 'partition_key': event.partition_key,
+ 'sequence_number': event.sequence_number,
+ 'offset': event.offset,
+ 'metadata': event.metadata
+ }
+
+ return json.dumps(event_dict)
diff --git a/tests/endtoend/http_v2_functions/fastapi_non_streaming_custom_resps/function_app.py b/tests/endtoend/http_v2_functions/fastapi_non_streaming_custom_resps/function_app.py
new file mode 100644
index 000000000..25900aafb
--- /dev/null
+++ b/tests/endtoend/http_v2_functions/fastapi_non_streaming_custom_resps/function_app.py
@@ -0,0 +1,108 @@
+import json
+import os
+import typing
+
+from azure.eventhub import EventData
+from azure.eventhub.aio import EventHubProducerClient
+
+import azure.functions as func
+
+app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
+
+
+# An HttpTrigger to generating EventHub event from EventHub Output Binding
+@app.function_name(name="eventhub_output")
+@app.route(route="eventhub_output")
+@app.event_hub_output(arg_name="event",
+ event_hub_name="python-worker-ci-eventhub-one",
+ connection="AzureWebJobsEventHubConnectionString")
+def eventhub_output(req: func.HttpRequest, event: func.Out[str]):
+ event.set(req.get_body().decode('utf-8'))
+ return 'OK'
+
+
+# This is an actual EventHub trigger which will convert the event data
+# into a storage blob.
+@app.function_name(name="eventhub_trigger")
+@app.event_hub_message_trigger(arg_name="event",
+ event_hub_name="python-worker-ci-eventhub-one",
+ connection="AzureWebJobsEventHubConnectionString"
+ )
+@app.blob_output(arg_name="$return",
+ path="python-worker-tests/test-eventhub-triggered.txt",
+ connection="AzureWebJobsStorage")
+def eventhub_trigger(event: func.EventHubEvent) -> bytes:
+ return event.get_body()
+
+
+# Retrieve the event data from storage blob and return it as Http response
+@app.function_name(name="get_eventhub_triggered")
+@app.route(route="get_eventhub_triggered")
+@app.blob_input(arg_name="file",
+ path="python-worker-tests/test-eventhub-triggered.txt",
+ connection="AzureWebJobsStorage")
+def get_eventhub_triggered(req: func.HttpRequest,
+ file: func.InputStream) -> str:
+ return file.read().decode('utf-8')
+
+
+# Retrieve the event data from storage blob and return it as Http response
+@app.function_name(name="get_metadata_triggered")
+@app.route(route="get_metadata_triggered")
+@app.blob_input(arg_name="file",
+ path="python-worker-tests/test-metadata-triggered.txt",
+ connection="AzureWebJobsStorage")
+async def get_metadata_triggered(req: func.HttpRequest,
+ file: func.InputStream) -> str:
+ return func.HttpResponse(body=file.read().decode('utf-8'),
+ status_code=200,
+ mimetype='application/json')
+
+
+# An HttpTrigger to generating EventHub event from azure-eventhub SDK.
+# Events generated from azure-eventhub contain the full metadata.
+@app.function_name(name="metadata_output")
+@app.route(route="metadata_output")
+async def metadata_output(req: func.HttpRequest):
+ # Parse event metadata from http request
+ json_string = req.get_body().decode('utf-8')
+ event_dict = json.loads(json_string)
+
+ # Create an EventHub Client and event batch
+ client = EventHubProducerClient.from_connection_string(
+ os.getenv('AzureWebJobsEventHubConnectionString'),
+ eventhub_name='python-worker-ci-eventhub-one-metadata')
+
+ # Generate new event based on http request with full metadata
+ event_data_batch = await client.create_batch()
+ event_data_batch.add(EventData(event_dict.get('body')))
+
+ # Send out event into event hub
+ try:
+ await client.send_batch(event_data_batch)
+ finally:
+ await client.close()
+
+ return 'OK'
+
+
+@app.function_name(name="metadata_trigger")
+@app.event_hub_message_trigger(
+ arg_name="event",
+ event_hub_name="python-worker-ci-eventhub-one-metadata",
+ connection="AzureWebJobsEventHubConnectionString")
+@app.blob_output(arg_name="$return",
+ path="python-worker-tests/test-metadata-triggered.txt",
+ connection="AzureWebJobsStorage")
+async def metadata_trigger(event: func.EventHubEvent) -> bytes:
+ event_dict: typing.Mapping[str, typing.Any] = {
+ 'body': event.get_body().decode('utf-8'),
+ # Uncomment this when the EnqueuedTimeUtc is fixed in azure-functions
+ # 'enqueued_time': event.enqueued_time.isoformat(),
+ 'partition_key': event.partition_key,
+ 'sequence_number': event.sequence_number,
+ 'offset': event.offset,
+ 'metadata': event.metadata
+ }
+
+ return json.dumps(event_dict)
diff --git a/tests/endtoend/http_v2_functions/fastapi_streaming_download_func/function_app.py b/tests/endtoend/http_v2_functions/fastapi_streaming_download_func/function_app.py
new file mode 100644
index 000000000..25900aafb
--- /dev/null
+++ b/tests/endtoend/http_v2_functions/fastapi_streaming_download_func/function_app.py
@@ -0,0 +1,108 @@
+import json
+import os
+import typing
+
+from azure.eventhub import EventData
+from azure.eventhub.aio import EventHubProducerClient
+
+import azure.functions as func
+
+app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
+
+
+# An HttpTrigger to generating EventHub event from EventHub Output Binding
+@app.function_name(name="eventhub_output")
+@app.route(route="eventhub_output")
+@app.event_hub_output(arg_name="event",
+ event_hub_name="python-worker-ci-eventhub-one",
+ connection="AzureWebJobsEventHubConnectionString")
+def eventhub_output(req: func.HttpRequest, event: func.Out[str]):
+ event.set(req.get_body().decode('utf-8'))
+ return 'OK'
+
+
+# This is an actual EventHub trigger which will convert the event data
+# into a storage blob.
+@app.function_name(name="eventhub_trigger")
+@app.event_hub_message_trigger(arg_name="event",
+ event_hub_name="python-worker-ci-eventhub-one",
+ connection="AzureWebJobsEventHubConnectionString"
+ )
+@app.blob_output(arg_name="$return",
+ path="python-worker-tests/test-eventhub-triggered.txt",
+ connection="AzureWebJobsStorage")
+def eventhub_trigger(event: func.EventHubEvent) -> bytes:
+ return event.get_body()
+
+
+# Retrieve the event data from storage blob and return it as Http response
+@app.function_name(name="get_eventhub_triggered")
+@app.route(route="get_eventhub_triggered")
+@app.blob_input(arg_name="file",
+ path="python-worker-tests/test-eventhub-triggered.txt",
+ connection="AzureWebJobsStorage")
+def get_eventhub_triggered(req: func.HttpRequest,
+ file: func.InputStream) -> str:
+ return file.read().decode('utf-8')
+
+
+# Retrieve the event data from storage blob and return it as Http response
+@app.function_name(name="get_metadata_triggered")
+@app.route(route="get_metadata_triggered")
+@app.blob_input(arg_name="file",
+ path="python-worker-tests/test-metadata-triggered.txt",
+ connection="AzureWebJobsStorage")
+async def get_metadata_triggered(req: func.HttpRequest,
+ file: func.InputStream) -> str:
+ return func.HttpResponse(body=file.read().decode('utf-8'),
+ status_code=200,
+ mimetype='application/json')
+
+
+# An HttpTrigger to generating EventHub event from azure-eventhub SDK.
+# Events generated from azure-eventhub contain the full metadata.
+@app.function_name(name="metadata_output")
+@app.route(route="metadata_output")
+async def metadata_output(req: func.HttpRequest):
+ # Parse event metadata from http request
+ json_string = req.get_body().decode('utf-8')
+ event_dict = json.loads(json_string)
+
+ # Create an EventHub Client and event batch
+ client = EventHubProducerClient.from_connection_string(
+ os.getenv('AzureWebJobsEventHubConnectionString'),
+ eventhub_name='python-worker-ci-eventhub-one-metadata')
+
+ # Generate new event based on http request with full metadata
+ event_data_batch = await client.create_batch()
+ event_data_batch.add(EventData(event_dict.get('body')))
+
+ # Send out event into event hub
+ try:
+ await client.send_batch(event_data_batch)
+ finally:
+ await client.close()
+
+ return 'OK'
+
+
+@app.function_name(name="metadata_trigger")
+@app.event_hub_message_trigger(
+ arg_name="event",
+ event_hub_name="python-worker-ci-eventhub-one-metadata",
+ connection="AzureWebJobsEventHubConnectionString")
+@app.blob_output(arg_name="$return",
+ path="python-worker-tests/test-metadata-triggered.txt",
+ connection="AzureWebJobsStorage")
+async def metadata_trigger(event: func.EventHubEvent) -> bytes:
+ event_dict: typing.Mapping[str, typing.Any] = {
+ 'body': event.get_body().decode('utf-8'),
+ # Uncomment this when the EnqueuedTimeUtc is fixed in azure-functions
+ # 'enqueued_time': event.enqueued_time.isoformat(),
+ 'partition_key': event.partition_key,
+ 'sequence_number': event.sequence_number,
+ 'offset': event.offset,
+ 'metadata': event.metadata
+ }
+
+ return json.dumps(event_dict)
diff --git a/tests/endtoend/http_v2_functions/fastapi_streaming_upload_func/function_app.py b/tests/endtoend/http_v2_functions/fastapi_streaming_upload_func/function_app.py
new file mode 100644
index 000000000..25900aafb
--- /dev/null
+++ b/tests/endtoend/http_v2_functions/fastapi_streaming_upload_func/function_app.py
@@ -0,0 +1,108 @@
+import json
+import os
+import typing
+
+from azure.eventhub import EventData
+from azure.eventhub.aio import EventHubProducerClient
+
+import azure.functions as func
+
+app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
+
+
+# An HttpTrigger to generating EventHub event from EventHub Output Binding
+@app.function_name(name="eventhub_output")
+@app.route(route="eventhub_output")
+@app.event_hub_output(arg_name="event",
+ event_hub_name="python-worker-ci-eventhub-one",
+ connection="AzureWebJobsEventHubConnectionString")
+def eventhub_output(req: func.HttpRequest, event: func.Out[str]):
+ event.set(req.get_body().decode('utf-8'))
+ return 'OK'
+
+
+# This is an actual EventHub trigger which will convert the event data
+# into a storage blob.
+@app.function_name(name="eventhub_trigger")
+@app.event_hub_message_trigger(arg_name="event",
+ event_hub_name="python-worker-ci-eventhub-one",
+ connection="AzureWebJobsEventHubConnectionString"
+ )
+@app.blob_output(arg_name="$return",
+ path="python-worker-tests/test-eventhub-triggered.txt",
+ connection="AzureWebJobsStorage")
+def eventhub_trigger(event: func.EventHubEvent) -> bytes:
+ return event.get_body()
+
+
+# Retrieve the event data from storage blob and return it as Http response
+@app.function_name(name="get_eventhub_triggered")
+@app.route(route="get_eventhub_triggered")
+@app.blob_input(arg_name="file",
+ path="python-worker-tests/test-eventhub-triggered.txt",
+ connection="AzureWebJobsStorage")
+def get_eventhub_triggered(req: func.HttpRequest,
+ file: func.InputStream) -> str:
+ return file.read().decode('utf-8')
+
+
+# Retrieve the event data from storage blob and return it as Http response
+@app.function_name(name="get_metadata_triggered")
+@app.route(route="get_metadata_triggered")
+@app.blob_input(arg_name="file",
+ path="python-worker-tests/test-metadata-triggered.txt",
+ connection="AzureWebJobsStorage")
+async def get_metadata_triggered(req: func.HttpRequest,
+ file: func.InputStream) -> str:
+ return func.HttpResponse(body=file.read().decode('utf-8'),
+ status_code=200,
+ mimetype='application/json')
+
+
+# An HttpTrigger to generating EventHub event from azure-eventhub SDK.
+# Events generated from azure-eventhub contain the full metadata.
+@app.function_name(name="metadata_output")
+@app.route(route="metadata_output")
+async def metadata_output(req: func.HttpRequest):
+ # Parse event metadata from http request
+ json_string = req.get_body().decode('utf-8')
+ event_dict = json.loads(json_string)
+
+ # Create an EventHub Client and event batch
+ client = EventHubProducerClient.from_connection_string(
+ os.getenv('AzureWebJobsEventHubConnectionString'),
+ eventhub_name='python-worker-ci-eventhub-one-metadata')
+
+ # Generate new event based on http request with full metadata
+ event_data_batch = await client.create_batch()
+ event_data_batch.add(EventData(event_dict.get('body')))
+
+ # Send out event into event hub
+ try:
+ await client.send_batch(event_data_batch)
+ finally:
+ await client.close()
+
+ return 'OK'
+
+
+@app.function_name(name="metadata_trigger")
+@app.event_hub_message_trigger(
+ arg_name="event",
+ event_hub_name="python-worker-ci-eventhub-one-metadata",
+ connection="AzureWebJobsEventHubConnectionString")
+@app.blob_output(arg_name="$return",
+ path="python-worker-tests/test-metadata-triggered.txt",
+ connection="AzureWebJobsStorage")
+async def metadata_trigger(event: func.EventHubEvent) -> bytes:
+ event_dict: typing.Mapping[str, typing.Any] = {
+ 'body': event.get_body().decode('utf-8'),
+ # Uncomment this when the EnqueuedTimeUtc is fixed in azure-functions
+ # 'enqueued_time': event.enqueued_time.isoformat(),
+ 'partition_key': event.partition_key,
+ 'sequence_number': event.sequence_number,
+ 'offset': event.offset,
+ 'metadata': event.metadata
+ }
+
+ return json.dumps(event_dict)
diff --git a/tests/unittests/http_functions/http_v2_functions/function_app.py b/tests/unittests/http_functions/http_v2_functions/function_app.py
new file mode 100644
index 000000000..f4bcfb36e
--- /dev/null
+++ b/tests/unittests/http_functions/http_v2_functions/function_app.py
@@ -0,0 +1,419 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+import asyncio
+import hashlib
+import json
+import logging
+import sys
+import time
+from urllib.request import urlopen
+from azure.functions.extension.fastapi import Request, Response
+import azure.functions as func
+
+app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
+
+logger = logging.getLogger("my-function")
+
+# request handling
+# request body, query, headers, and route params
+# request validation errors
+# diff http verbs
+
+# response handling
+# response body, status code, headers
+# error responses
+
+# edge cases
+# invalid requests sent behavior with missing body, query, headers, and route params
+# request payload exceeds max size
+# request payload contains special characters
+
+@app.route(route="return_str")
+def return_str(req: Request) -> str:
+ return 'Hello World!'
+
+#
+# @app.route(route="accept_json")
+# def accept_json(req: Request):
+# return json.dumps({
+# 'method': req.method,
+# 'url': req.url,
+# 'headers': dict(req.headers),
+# 'params': dict(req.params),
+# 'get_body': req.get_body().decode(),
+# 'get_json': req.get_json()
+# })
+#
+#
+# async def nested():
+# try:
+# 1 / 0
+# except ZeroDivisionError:
+# logger.error('and another error', exc_info=True)
+#
+#
+# @app.route(route="async_logging")
+# async def async_logging(req: Request):
+# logger.info('hello %s', 'info')
+#
+# await asyncio.sleep(0.1)
+#
+# # Create a nested task to check if invocation_id is still
+# # logged correctly.
+# await asyncio.ensure_future(nested())
+#
+# await asyncio.sleep(0.1)
+#
+# return 'OK-async'
+#
+#
+# @app.route(route="async_return_str")
+# async def async_return_str(req: Request):
+# await asyncio.sleep(0.1)
+# return 'Hello Async World!'
+#
+#
+# @app.route(route="debug_logging")
+# def debug_logging(req: Request):
+# logging.critical('logging critical', exc_info=True)
+# logging.info('logging info', exc_info=True)
+# logging.warning('logging warning', exc_info=True)
+# logging.debug('logging debug', exc_info=True)
+# logging.error('logging error', exc_info=True)
+# return 'OK-debug'
+#
+#
+# @app.route(route="debug_user_logging")
+# def debug_user_logging(req: Request):
+# logger.setLevel(logging.DEBUG)
+#
+# logging.critical('logging critical', exc_info=True)
+# logger.info('logging info', exc_info=True)
+# logger.warning('logging warning', exc_info=True)
+# logger.debug('logging debug', exc_info=True)
+# logger.error('logging error', exc_info=True)
+# return 'OK-user-debug'
+#
+#
+# # Attempt to log info into system log from customer code
+# disguised_logger = logging.getLogger('azure_functions_worker')
+#
+#
+# async def parallelly_print():
+# await asyncio.sleep(0.1)
+# print('parallelly_print')
+#
+#
+# async def parallelly_log_info():
+# await asyncio.sleep(0.2)
+# logging.info('parallelly_log_info at root logger')
+#
+#
+# async def parallelly_log_warning():
+# await asyncio.sleep(0.3)
+# logging.warning('parallelly_log_warning at root logger')
+#
+#
+# async def parallelly_log_error():
+# await asyncio.sleep(0.4)
+# logging.error('parallelly_log_error at root logger')
+#
+#
+# async def parallelly_log_exception():
+# await asyncio.sleep(0.5)
+# try:
+# raise Exception('custom exception')
+# except Exception:
+# logging.exception('parallelly_log_exception at root logger',
+# exc_info=sys.exc_info())
+#
+#
+# async def parallelly_log_custom():
+# await asyncio.sleep(0.6)
+# logger.info('parallelly_log_custom at custom_logger')
+#
+#
+# async def parallelly_log_system():
+# await asyncio.sleep(0.7)
+# disguised_logger.info('parallelly_log_system at disguised_logger')
+#
+#
+# @app.route(route="hijack_current_event_loop")
+# async def hijack_current_event_loop(req: Request) -> Response:
+# loop = asyncio.get_event_loop()
+#
+# # Create multiple tasks and schedule it into one asyncio.wait blocker
+# task_print: asyncio.Task = loop.create_task(parallelly_print())
+# task_info: asyncio.Task = loop.create_task(parallelly_log_info())
+# task_warning: asyncio.Task = loop.create_task(parallelly_log_warning())
+# task_error: asyncio.Task = loop.create_task(parallelly_log_error())
+# task_exception: asyncio.Task = loop.create_task(parallelly_log_exception())
+# task_custom: asyncio.Task = loop.create_task(parallelly_log_custom())
+# task_disguise: asyncio.Task = loop.create_task(parallelly_log_system())
+#
+# # Create an awaitable future and occupy the current event loop resource
+# future = loop.create_future()
+# loop.call_soon_threadsafe(future.set_result, 'callsoon_log')
+#
+# # WaitAll
+# await asyncio.wait([task_print, task_info, task_warning, task_error,
+# task_exception, task_custom, task_disguise, future])
+#
+# # Log asyncio low-level future result
+# logging.info(future.result())
+#
+# return 'OK-hijack-current-event-loop'
+#
+#
+# @app.route(route="no_return")
+# def no_return(req: Request):
+# logger.info('hi')
+#
+#
+# @app.route(route="no_return_returns")
+# def no_return_returns(req):
+# return 'ABC'
+#
+#
+# @app.route(route="print_logging")
+# def print_logging(req: Request):
+# flush_required = False
+# is_console_log = False
+# is_stderr = False
+# message = req.params.get('message', '')
+#
+# if req.params.get('flush') == 'true':
+# flush_required = True
+# if req.params.get('console') == 'true':
+# is_console_log = True
+# if req.params.get('is_stderr') == 'true':
+# is_stderr = True
+#
+# # Adding LanguageWorkerConsoleLog will make function host to treat
+# # this as system log and will be propagated to kusto
+# prefix = 'LanguageWorkerConsoleLog' if is_console_log else ''
+# print(f'{prefix} {message}'.strip(),
+# file=sys.stderr if is_stderr else sys.stdout,
+# flush=flush_required)
+#
+# return 'OK-print-logging'
+#
+#
+# @app.route(route="raw_body_bytes")
+# def raw_body_bytes(req: Request) -> Response:
+# body = req.get_body()
+# body_len = str(len(body))
+#
+# headers = {'body-len': body_len}
+# return Response(body=body, status_code=200, headers=headers)
+#
+#
+# @app.route(route="remapped_context")
+# def remapped_context(req: Request):
+# return req.method
+#
+#
+# @app.route(route="return_bytes")
+# def return_bytes(req: Request):
+# # This function will fail, as we don't auto-convert "bytes" to "http".
+# return b'Hello World!'
+#
+#
+# @app.route(route="return_context")
+# def return_context(req: Request, context: func.Context):
+# return json.dumps({
+# 'method': req.method,
+# 'ctx_func_name': context.function_name,
+# 'ctx_func_dir': context.function_directory,
+# 'ctx_invocation_id': context.invocation_id,
+# 'ctx_trace_context_Traceparent': context.trace_context.Traceparent,
+# 'ctx_trace_context_Tracestate': context.trace_context.Tracestate,
+# })
+#
+#
+# @app.route(route="return_http")
+# def return_http(req: Request):
+# return Response('
Hello World™
',
+# mimetype='text/html')
+#
+#
+# @app.route(route="return_http_404")
+# def return_http_404(req: Request):
+# return Response('bye', status_code=404)
+#
+#
+# @app.route(route="return_http_auth_admin", auth_level=func.AuthLevel.ADMIN)
+# def return_http_auth_admin(req: Request):
+# return Response('Hello World™
',
+# mimetype='text/html')
+#
+#
+# @app.route(route="return_http_no_body")
+# def return_http_no_body(req: Request):
+# return Response()
+#
+#
+# @app.route(route="return_http_redirect")
+# def return_http_redirect(req: Request):
+# location = 'return_http?code={}'.format(req.params['code'])
+# return Response(
+# status_code=302,
+# headers={'location': location})
+#
+#
+# @app.route(route="return_out", binding_arg_name="foo")
+# def return_out(req: Request, foo: func.Out[Response]):
+# foo.set(Response(body='hello', status_code=201))
+#
+#
+# @app.route(route="return_request")
+# def return_request(req: Request):
+# params = dict(req.params)
+# params.pop('code', None)
+# body = req.get_body()
+# return json.dumps({
+# 'method': req.method,
+# 'url': req.url,
+# 'headers': dict(req.headers),
+# 'params': params,
+# 'get_body': body.decode(),
+# 'body_hash': hashlib.sha256(body).hexdigest(),
+# })
+#
+#
+# @app.route(route="return_route_params/{param1}/{param2}")
+# def return_route_params(req: Request) -> str:
+# return json.dumps(dict(req.route_params))
+#
+#
+# @app.route(route="sync_logging")
+# def main(req: Request):
+# try:
+# 1 / 0
+# except ZeroDivisionError:
+# logger.error('a gracefully handled error', exc_info=True)
+# logger.error('a gracefully handled critical error', exc_info=True)
+# time.sleep(0.05)
+# return 'OK-sync'
+#
+#
+# @app.route(route="unhandled_error")
+# def unhandled_error(req: Request):
+# 1 / 0
+#
+#
+# @app.route(route="unhandled_urllib_error")
+# def unhandled_urllib_error(req: Request) -> str:
+# image_url = req.params.get('img')
+# urlopen(image_url).read()
+#
+#
+# class UnserializableException(Exception):
+# def __str__(self):
+# raise RuntimeError('cannot serialize me')
+#
+#
+# @app.route(route="unhandled_unserializable_error")
+# def unhandled_unserializable_error(req: Request) -> str:
+# raise UnserializableException('foo')
+#
+#
+# async def try_log():
+# logger.info("try_log")
+#
+#
+# @app.route(route="user_event_loop")
+# def user_event_loop(req: Request) -> Response:
+# loop = asyncio.SelectorEventLoop()
+# asyncio.set_event_loop(loop)
+#
+# # This line should throws an asyncio RuntimeError exception
+# loop.run_until_complete(try_log())
+# loop.close()
+# return 'OK-user-event-loop'
+#
+#
+# @app.route(route="multiple_set_cookie_resp_headers")
+# def multiple_set_cookie_resp_headers(
+# req: Request) -> Response:
+# logging.info('Python HTTP trigger function processed a request.')
+# resp = Response(
+# "This HTTP triggered function executed successfully.")
+#
+# resp.headers.add("Set-Cookie",
+# 'foo3=42; Domain=example.com; Expires=Thu, 12-Jan-2017 '
+# '13:55:08 GMT; Path=/; Max-Age=10000000; Secure; '
+# 'HttpOnly')
+# resp.headers.add("Set-Cookie",
+# 'foo3=43; Domain=example.com; Expires=Thu, 12-Jan-2018 '
+# '13:55:08 GMT; Path=/; Max-Age=10000000; Secure; '
+# 'HttpOnly')
+# resp.headers.add("HELLO", 'world')
+#
+# return resp
+#
+#
+# @app.route(route="response_cookie_header_nullable_bool_err")
+# def response_cookie_header_nullable_bool_err(
+# req: Request) -> Response:
+# logging.info('Python HTTP trigger function processed a request.')
+# resp = Response(
+# "This HTTP triggered function executed successfully.")
+#
+# resp.headers.add("Set-Cookie",
+# 'foo3=42; Domain=example.com; Expires=Thu, 12-Jan-2017 '
+# '13:55:08 GMT; Path=/; Max-Age=10000000; SecureFalse; '
+# 'HttpOnly')
+#
+# return resp
+#
+#
+# @app.route(route="response_cookie_header_nullable_double_err")
+# def response_cookie_header_nullable_double_err(
+# req: Request) -> Response:
+# logging.info('Python HTTP trigger function processed a request.')
+# resp = Response(
+# "This HTTP triggered function executed successfully.")
+#
+# resp.headers.add("Set-Cookie",
+# 'foo3=42; Domain=example.com; Expires=Thu, 12-Jan-2017 '
+# '13:55:08 GMT; Path=/; Max-Age=Dummy; SecureFalse; '
+# 'HttpOnly')
+#
+# return resp
+#
+#
+# @app.route(route="response_cookie_header_nullable_timestamp_err")
+# def response_cookie_header_nullable_timestamp_err(
+# req: Request) -> Response:
+# logging.info('Python HTTP trigger function processed a request.')
+# resp = Response(
+# "This HTTP triggered function executed successfully.")
+#
+# resp.headers.add("Set-Cookie", 'foo=bar; Domain=123; Expires=Dummy')
+#
+# return resp
+#
+#
+# @app.route(route="set_cookie_resp_header_default_values")
+# def set_cookie_resp_header_default_values(
+# req: Request) -> Response:
+# logging.info('Python HTTP trigger function processed a request.')
+# resp = Response(
+# "This HTTP triggered function executed successfully.")
+#
+# resp.headers.add("Set-Cookie", 'foo=bar')
+#
+# return resp
+#
+#
+# @app.route(route="set_cookie_resp_header_empty")
+# def set_cookie_resp_header_empty(
+# req: Request) -> Response:
+# logging.info('Python HTTP trigger function processed a request.')
+# resp = Response(
+# "This HTTP triggered function executed successfully.")
+#
+# resp.headers.add("Set-Cookie", '')
+#
+# return resp
diff --git a/tests/unittests/test_http_functions.py b/tests/unittests/test_http_functions.py
index 6d4ecbe1d..344ffdce8 100644
--- a/tests/unittests/test_http_functions.py
+++ b/tests/unittests/test_http_functions.py
@@ -10,7 +10,6 @@
from tests.utils import testutils
-
class TestHttpFunctions(testutils.WebHostTestCase):
@classmethod
@@ -462,3 +461,9 @@ def test_no_return(self):
def test_no_return_returns(self):
r = self.webhost.request('GET', 'no_return_returns')
self.assertEqual(r.status_code, 200)
+
+class TestHttpFunctionsV2(TestHttpFunctions):
+ @classmethod
+ def get_script_dir(cls):
+ return testutils.UNIT_TESTS_FOLDER / 'http_functions' / \
+ 'http_v2_functions'
From 9922b2ce607ade3149f95fe21eb7765dcab061ed Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Thu, 4 Apr 2024 22:41:59 -0700
Subject: [PATCH 002/101] final changes and tests
---
azure_functions_worker/constants.py | 2 +-
azure_functions_worker/dispatcher.py | 55 ++-
azure_functions_worker/http_proxy.py | 5 +-
azure_functions_worker/loader.py | 2 +-
azure_functions_worker/logging.py | 4 +-
python/test/worker.config.json | 2 +-
setup.py | 2 +-
.../test_linux_consumption.py | 35 ++
.../fastapi/file_name/main.py | 48 ++
.../http_functions_v2/fastapi/function_app.py | 91 ++++
.../fastapi_data_class/function_app.py | 108 -----
.../function_app.py | 108 -----
.../function_app.py | 108 -----
.../function_app.py | 108 -----
tests/endtoend/test_http_functions.py | 166 ++++++-
.../http_v2_functions/fastapi/function_app.py | 434 +++++++++++++++++
.../http_v2_functions/function_app.py | 419 ----------------
tests/unittests/test_http_functions.py | 5 -
tests/unittests/test_http_functions_v2.py | 457 ++++++++++++++++++
19 files changed, 1261 insertions(+), 898 deletions(-)
create mode 100644 tests/endtoend/http_functions/http_functions_v2/fastapi/file_name/main.py
create mode 100644 tests/endtoend/http_functions/http_functions_v2/fastapi/function_app.py
delete mode 100644 tests/endtoend/http_v2_functions/fastapi_data_class/function_app.py
delete mode 100644 tests/endtoend/http_v2_functions/fastapi_non_streaming_custom_resps/function_app.py
delete mode 100644 tests/endtoend/http_v2_functions/fastapi_streaming_download_func/function_app.py
delete mode 100644 tests/endtoend/http_v2_functions/fastapi_streaming_upload_func/function_app.py
create mode 100644 tests/unittests/http_functions/http_v2_functions/fastapi/function_app.py
delete mode 100644 tests/unittests/http_functions/http_v2_functions/function_app.py
create mode 100644 tests/unittests/test_http_functions_v2.py
diff --git a/azure_functions_worker/constants.py b/azure_functions_worker/constants.py
index 7f09d586c..b38794017 100644
--- a/azure_functions_worker/constants.py
+++ b/azure_functions_worker/constants.py
@@ -63,7 +63,7 @@
METADATA_PROPERTIES_WORKER_INDEXED = "worker_indexed"
# HostNames
-LOCAL_HOST = "localhost"
+LOCAL_HOST = "127.0.0.1"
# Header names
X_MS_INVOCATION_ID = "x-ms-invocation-id"
diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py
index 35143fb68..641b2a25e 100644
--- a/azure_functions_worker/dispatcher.py
+++ b/azure_functions_worker/dispatcher.py
@@ -91,6 +91,7 @@ def __init__(self, loop: BaseEventLoop, host: str, port: int,
self._shmem_mgr = SharedMemoryManager()
self._old_task_factory = None
self.function_metadata_result = None
+ self._has_http_func = False
# Used to store metadata returns
self._function_metadata_result = None
@@ -516,6 +517,7 @@ async def _handle__function_load_request(self, request):
async def _handle__invocation_request(self, request):
invocation_time = datetime.utcnow()
invoc_request = request.invocation_request
+ trigger_metadata = invoc_request.trigger_metadata
invocation_id = invoc_request.invocation_id
function_id = invoc_request.function_id
@@ -560,6 +562,7 @@ async def _handle__invocation_request(self, request):
pytype=pb_type_info.pytype,
shmem_mgr=self._shmem_mgr)
+ http_v2_enabled = False
if fi.trigger_metadata.get('type') == HTTP_TRIGGER:
from azure.functions.extension.base import HttpV2FeatureChecker
http_v2_enabled = HttpV2FeatureChecker.http_v2_enabled()
@@ -567,6 +570,11 @@ async def _handle__invocation_request(self, request):
if http_v2_enabled:
http_request = await http_coordinator.get_http_request_async(
invocation_id)
+
+ from azure.functions.extension.base import RequestTrackerMeta
+ route_params = {key: item.string for key, item in trigger_metadata.items() if key not in ['Headers', 'Query']}
+
+ RequestTrackerMeta.get_synchronizer().sync_route_params(http_request, route_params)
args[fi.trigger_metadata.get('param_name')] = http_request
fi_context = self._get_context(invoc_request, fi.name, fi.directory)
@@ -581,23 +589,26 @@ async def _handle__invocation_request(self, request):
for name in fi.output_types:
args[name] = bindings.Out()
- if fi.is_async:
- call_result = await self._run_async_func(
- fi_context, fi.func, args
- )
-
- else:
- call_result = await self._loop.run_in_executor(
- self._sync_call_tp,
- self._run_sync_func,
- invocation_id, fi_context, fi.func, args)
-
- if call_result is not None and not fi.has_return:
- raise RuntimeError(f'function {fi.name!r} without a $return '
- 'binding returned a non-None value')
-
- if http_v2_enabled:
- http_coordinator.set_http_response(invocation_id, call_result)
+ call_result = None
+ call_error = None
+ try:
+ if fi.is_async:
+ call_result = await self._run_async_func(fi_context, fi.func, args)
+ else:
+ call_result = await self._loop.run_in_executor(
+ self._sync_call_tp,
+ self._run_sync_func,
+ invocation_id, fi_context, fi.func, args)
+
+ if call_result is not None and not fi.has_return:
+ raise RuntimeError(f'function {fi.name!r} without a $return '
+ 'binding returned a non-None value')
+ except Exception as e:
+ call_error = e
+ raise
+ finally:
+ if http_v2_enabled:
+ http_coordinator.set_http_response(invocation_id, call_result if call_result is not None else call_error)
output_data = []
cache_enabled = self._function_data_cache_enabled
@@ -753,9 +764,15 @@ async def catch_all(request: request_type): # type: ignore
invoc_id = request.headers.get(X_MS_INVOCATION_ID)
if invoc_id is None:
raise ValueError(f"Header {X_MS_INVOCATION_ID} not found")
-
+ logger.info('Received HTTP request for invocation %s', invoc_id)
http_coordinator.set_http_request(invoc_id, request)
http_resp = await http_coordinator.await_http_response_async(invoc_id)
+
+ logger.info('Sending HTTP response for invocation %s', invoc_id)
+ # if http_resp is an python exception, raise it
+ if isinstance(http_resp, Exception):
+ raise http_resp
+
return http_resp
web_server = web_server_class(LOCAL_HOST, unused_port, app)
@@ -763,6 +780,7 @@ async def catch_all(request: request_type): # type: ignore
loop = asyncio.get_event_loop()
loop.create_task(web_server_run_task)
+ logger.info('HTTP server starting on %s:%s', LOCAL_HOST, unused_port)
return f"http://{LOCAL_HOST}:{unused_port}"
@@ -779,7 +797,6 @@ def index_functions(self, function_path: str):
indexed_functions)
indexed_function_logs: List[str] = []
- self._has_http_func = False
for func in indexed_functions:
self._has_http_func = self._has_http_func or func.is_http_function()
function_log = "Function Name: {}, Function Binding: {}" \
diff --git a/azure_functions_worker/http_proxy.py b/azure_functions_worker/http_proxy.py
index eff55067e..b56213485 100644
--- a/azure_functions_worker/http_proxy.py
+++ b/azure_functions_worker/http_proxy.py
@@ -102,14 +102,12 @@ def set_http_request(self, invoc_id, http_request):
self._context_references[invoc_id] = AsyncContextReference()
context_ref = self._context_references.get(invoc_id)
context_ref.http_request = http_request
- context_ref.http_request_available_event.set()
def set_http_response(self, invoc_id, http_response):
if invoc_id not in self._context_references:
raise Exception("No context reference found for invocation %s", invoc_id)
context_ref = self._context_references.get(invoc_id)
context_ref.http_response = http_response
- context_ref.http_response_available_event.set()
async def get_http_request_async(self, invoc_id):
if invoc_id not in self._context_references:
@@ -141,8 +139,7 @@ def _pop_http_response(self, invoc_id):
if response is not None:
context_ref.http_response = None
return response
-
- raise Exception("No http response found for invocation %s", invoc_id)
+ # If user does not set the response, return nothing and web server will return 200 empty response
http_coordinator = HttpCoordinator()
diff --git a/azure_functions_worker/loader.py b/azure_functions_worker/loader.py
index da706fffd..e90888e3c 100644
--- a/azure_functions_worker/loader.py
+++ b/azure_functions_worker/loader.py
@@ -146,7 +146,7 @@ def process_indexed_function(functions_registry: functions.Registry,
properties={METADATA_PROPERTIES_WORKER_INDEXED: "True"})
fx_metadata_results.append(function_metadata)
- return fx_metadata_results
+ return fx_metadata_results
except Exception as e:
logger.error(f'Error in process_indexed_function. {e}', exc_info=True)
raise e
diff --git a/azure_functions_worker/logging.py b/azure_functions_worker/logging.py
index 7a5340919..ddc5a7faf 100644
--- a/azure_functions_worker/logging.py
+++ b/azure_functions_worker/logging.py
@@ -20,8 +20,8 @@
handler: Optional[logging.Handler] = None
error_handler: Optional[logging.Handler] = None
-local_handler = logging.FileHandler("E:/projects/AzureFunctionsPythonWorker/log.txt")
-logger.addHandler(local_handler)
+# local_handler = logging.FileHandler("E:/projects/AzureFunctionsPythonWorker/log.txt")
+# logger.addHandler(local_handler)
def format_exception(exception: Exception) -> str:
msg = str(exception) + "\n"
diff --git a/python/test/worker.config.json b/python/test/worker.config.json
index 05f6d26ed..d530908fc 100644
--- a/python/test/worker.config.json
+++ b/python/test/worker.config.json
@@ -2,7 +2,7 @@
"description":{
"language":"python",
"extensions":[".py"],
- "defaultExecutablePath":"E:\\projects\\AzureFunctionsPythonWorker\\.venv_3.8\\Scripts\\python.exe",
+ "defaultExecutablePath":"E:\\projects\\AzureFunctionsPythonWorker\\.venv_3.9_debug\\Scripts\\python.exe",
"defaultWorkerPath":"worker.py",
"workerIndexing": "true"
}
diff --git a/setup.py b/setup.py
index f85f1c332..a4f3095a0 100644
--- a/setup.py
+++ b/setup.py
@@ -110,7 +110,7 @@
"numpy",
"pre-commit"
],
- "fastapi": ["azure-functions-extension-fastapi"]
+ "http-v2": ["azure-functions-extension-fastapi", "ujson", "orjson"]
}
diff --git a/tests/consumption_tests/test_linux_consumption.py b/tests/consumption_tests/test_linux_consumption.py
index 195c1591c..3c7232366 100644
--- a/tests/consumption_tests/test_linux_consumption.py
+++ b/tests/consumption_tests/test_linux_consumption.py
@@ -336,6 +336,41 @@ def test_reload_variables_after_oom_error(self):
self.assertNotIn("Failure Exception: ModuleNotFoundError",
logs)
+ @skipIf(sys.version_info.minor != 10,
+ "This is testing only for python310")
+ def test_http_v2_fastapi_streaming_upload_download(self):
+ """
+ A function app with init indexing enabled
+ """
+ import random as rand
+ with LinuxConsumptionWebHostController(_DEFAULT_HOST_VERSION,
+ self._py_version) as ctrl:
+ ctrl.assign_container(env={
+ "AzureWebJobsStorage": self._storage,
+ "SCM_RUN_FROM_PACKAGE": self._get_blob_url("HttpV2FastApiStreaming"),
+ PYTHON_ENABLE_INIT_INDEXING: "true",
+ PYTHON_ISOLATE_WORKER_DEPENDENCIES: "1"
+ })
+
+ def generate_random_bytes_stream():
+ """Generate a stream of random bytes."""
+ yield b'streaming'
+ yield b'testing'
+ yield b'response'
+ yield b'is'
+ yield b'returned'
+
+ req = Request('POST', f'{ctrl.url}/api/http_v2_fastapi_streaming', data=generate_random_bytes_stream())
+ resp = ctrl.send_request(req)
+ self.assertEqual(resp.status_code, 200)
+
+ streamed_data = b''
+ for chunk in resp.iter_content(chunk_size=1024):
+ if chunk:
+ streamed_data+= chunk
+
+ self.assertEqual(streamed_data, b'streamingtestingresponseisreturned')
+
def _get_blob_url(self, scenario_name: str) -> str:
return (
f'https://pythonworker{self._py_shortform}sa.blob.core.windows.net/'
diff --git a/tests/endtoend/http_functions/http_functions_v2/fastapi/file_name/main.py b/tests/endtoend/http_functions/http_functions_v2/fastapi/file_name/main.py
new file mode 100644
index 000000000..ad2831f0a
--- /dev/null
+++ b/tests/endtoend/http_functions/http_functions_v2/fastapi/file_name/main.py
@@ -0,0 +1,48 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+from datetime import datetime
+import logging
+import time
+
+import azure.functions as func
+
+from azure.functions.extension.fastapi import Request, Response, StreamingResponse, \
+ HTMLResponse, PlainTextResponse, HTMLResponse, JSONResponse, \
+ UJSONResponse, ORJSONResponse, RedirectResponse, FileResponse
+
+app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
+
+
+@app.route(route="default_template")
+async def default_template(req: Request) -> Response:
+ logging.info('Python HTTP trigger function processed a request.')
+
+ name = req.query_params.get('name')
+ if not name:
+ try:
+ req_body = await req.json()
+ except ValueError:
+ pass
+ else:
+ name = req_body.get('name')
+
+ if name:
+ return Response(
+ f"Hello, {name}. This HTTP triggered function "
+ f"executed successfully.")
+ else:
+ return Response(
+ "This HTTP triggered function executed successfully. "
+ "Pass a name in the query string or in the request body for a"
+ " personalized response.",
+ status_code=200
+ )
+
+
+@app.route(route="http_func")
+def http_func(req: Request) -> Response:
+ time.sleep(1)
+
+ current_time = datetime.now().strftime("%H:%M:%S")
+ return Response(f"{current_time}")
diff --git a/tests/endtoend/http_functions/http_functions_v2/fastapi/function_app.py b/tests/endtoend/http_functions/http_functions_v2/fastapi/function_app.py
new file mode 100644
index 000000000..1870767da
--- /dev/null
+++ b/tests/endtoend/http_functions/http_functions_v2/fastapi/function_app.py
@@ -0,0 +1,91 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+import asyncio
+from datetime import datetime
+import logging
+import time
+
+import azure.functions as func
+from azure.functions.extension.fastapi import Request, Response, StreamingResponse, \
+ HTMLResponse, PlainTextResponse, HTMLResponse, JSONResponse, \
+ UJSONResponse, ORJSONResponse, RedirectResponse, FileResponse
+
+app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
+
+
+@app.route(route="default_template")
+async def default_template(req: Request) -> Response:
+ logging.info('Python HTTP trigger function processed a request.')
+
+ name = req.query_params.get('name')
+ if not name:
+ try:
+ req_body = await req.json()
+ except ValueError:
+ pass
+ else:
+ name = req_body.get('name')
+
+ if name:
+ return Response(
+ f"Hello, {name}. This HTTP triggered function "
+ f"executed successfully.")
+ else:
+ return Response(
+ "This HTTP triggered function executed successfully. "
+ "Pass a name in the query string or in the request body for a"
+ " personalized response.",
+ status_code=200
+ )
+
+
+@app.route(route="http_func")
+def http_func(req: Request) -> Response:
+ time.sleep(1)
+
+ current_time = datetime.now().strftime("%H:%M:%S")
+ return Response(f"{current_time}")
+
+
+@app.route(route="upload_data_stream")
+async def upload_data_stream(req: Request) -> Response:
+ # Define a list to accumulate the streaming data
+ data_chunks = []
+
+ async def process_stream():
+ async for chunk in req.stream():
+ # Append each chunk of streaming data to the list
+ data_chunks.append(chunk)
+
+ await process_stream()
+
+ # Concatenate the data chunks to form the complete data
+ complete_data = b"".join(data_chunks)
+
+ # Return the complete data as the response
+ return Response(content=complete_data, status_code=200)
+
+
+@app.route(route="return_streaming")
+async def return_streaming(req: Request) -> StreamingResponse:
+ async def content():
+ yield b"First chunk\n"
+ yield b"Second chunk\n"
+ return StreamingResponse(content())
+
+@app.route(route="return_html")
+def return_html(req: Request) -> HTMLResponse:
+ html_content = "Hello, World!
"
+ return HTMLResponse(content=html_content, status_code=200)
+
+@app.route(route="return_ujson")
+def return_ujson(req: Request) -> UJSONResponse:
+ return UJSONResponse(content={"message": "Hello, World!"}, status_code=200)
+
+@app.route(route="return_orjson")
+def return_orjson(req: Request) -> ORJSONResponse:
+ return ORJSONResponse(content={"message": "Hello, World!"}, status_code=200)
+
+@app.route(route="return_file")
+def return_file(req: Request) -> FileResponse:
+ return FileResponse("function_app.py")
\ No newline at end of file
diff --git a/tests/endtoend/http_v2_functions/fastapi_data_class/function_app.py b/tests/endtoend/http_v2_functions/fastapi_data_class/function_app.py
deleted file mode 100644
index 25900aafb..000000000
--- a/tests/endtoend/http_v2_functions/fastapi_data_class/function_app.py
+++ /dev/null
@@ -1,108 +0,0 @@
-import json
-import os
-import typing
-
-from azure.eventhub import EventData
-from azure.eventhub.aio import EventHubProducerClient
-
-import azure.functions as func
-
-app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
-
-
-# An HttpTrigger to generating EventHub event from EventHub Output Binding
-@app.function_name(name="eventhub_output")
-@app.route(route="eventhub_output")
-@app.event_hub_output(arg_name="event",
- event_hub_name="python-worker-ci-eventhub-one",
- connection="AzureWebJobsEventHubConnectionString")
-def eventhub_output(req: func.HttpRequest, event: func.Out[str]):
- event.set(req.get_body().decode('utf-8'))
- return 'OK'
-
-
-# This is an actual EventHub trigger which will convert the event data
-# into a storage blob.
-@app.function_name(name="eventhub_trigger")
-@app.event_hub_message_trigger(arg_name="event",
- event_hub_name="python-worker-ci-eventhub-one",
- connection="AzureWebJobsEventHubConnectionString"
- )
-@app.blob_output(arg_name="$return",
- path="python-worker-tests/test-eventhub-triggered.txt",
- connection="AzureWebJobsStorage")
-def eventhub_trigger(event: func.EventHubEvent) -> bytes:
- return event.get_body()
-
-
-# Retrieve the event data from storage blob and return it as Http response
-@app.function_name(name="get_eventhub_triggered")
-@app.route(route="get_eventhub_triggered")
-@app.blob_input(arg_name="file",
- path="python-worker-tests/test-eventhub-triggered.txt",
- connection="AzureWebJobsStorage")
-def get_eventhub_triggered(req: func.HttpRequest,
- file: func.InputStream) -> str:
- return file.read().decode('utf-8')
-
-
-# Retrieve the event data from storage blob and return it as Http response
-@app.function_name(name="get_metadata_triggered")
-@app.route(route="get_metadata_triggered")
-@app.blob_input(arg_name="file",
- path="python-worker-tests/test-metadata-triggered.txt",
- connection="AzureWebJobsStorage")
-async def get_metadata_triggered(req: func.HttpRequest,
- file: func.InputStream) -> str:
- return func.HttpResponse(body=file.read().decode('utf-8'),
- status_code=200,
- mimetype='application/json')
-
-
-# An HttpTrigger to generating EventHub event from azure-eventhub SDK.
-# Events generated from azure-eventhub contain the full metadata.
-@app.function_name(name="metadata_output")
-@app.route(route="metadata_output")
-async def metadata_output(req: func.HttpRequest):
- # Parse event metadata from http request
- json_string = req.get_body().decode('utf-8')
- event_dict = json.loads(json_string)
-
- # Create an EventHub Client and event batch
- client = EventHubProducerClient.from_connection_string(
- os.getenv('AzureWebJobsEventHubConnectionString'),
- eventhub_name='python-worker-ci-eventhub-one-metadata')
-
- # Generate new event based on http request with full metadata
- event_data_batch = await client.create_batch()
- event_data_batch.add(EventData(event_dict.get('body')))
-
- # Send out event into event hub
- try:
- await client.send_batch(event_data_batch)
- finally:
- await client.close()
-
- return 'OK'
-
-
-@app.function_name(name="metadata_trigger")
-@app.event_hub_message_trigger(
- arg_name="event",
- event_hub_name="python-worker-ci-eventhub-one-metadata",
- connection="AzureWebJobsEventHubConnectionString")
-@app.blob_output(arg_name="$return",
- path="python-worker-tests/test-metadata-triggered.txt",
- connection="AzureWebJobsStorage")
-async def metadata_trigger(event: func.EventHubEvent) -> bytes:
- event_dict: typing.Mapping[str, typing.Any] = {
- 'body': event.get_body().decode('utf-8'),
- # Uncomment this when the EnqueuedTimeUtc is fixed in azure-functions
- # 'enqueued_time': event.enqueued_time.isoformat(),
- 'partition_key': event.partition_key,
- 'sequence_number': event.sequence_number,
- 'offset': event.offset,
- 'metadata': event.metadata
- }
-
- return json.dumps(event_dict)
diff --git a/tests/endtoend/http_v2_functions/fastapi_non_streaming_custom_resps/function_app.py b/tests/endtoend/http_v2_functions/fastapi_non_streaming_custom_resps/function_app.py
deleted file mode 100644
index 25900aafb..000000000
--- a/tests/endtoend/http_v2_functions/fastapi_non_streaming_custom_resps/function_app.py
+++ /dev/null
@@ -1,108 +0,0 @@
-import json
-import os
-import typing
-
-from azure.eventhub import EventData
-from azure.eventhub.aio import EventHubProducerClient
-
-import azure.functions as func
-
-app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
-
-
-# An HttpTrigger to generating EventHub event from EventHub Output Binding
-@app.function_name(name="eventhub_output")
-@app.route(route="eventhub_output")
-@app.event_hub_output(arg_name="event",
- event_hub_name="python-worker-ci-eventhub-one",
- connection="AzureWebJobsEventHubConnectionString")
-def eventhub_output(req: func.HttpRequest, event: func.Out[str]):
- event.set(req.get_body().decode('utf-8'))
- return 'OK'
-
-
-# This is an actual EventHub trigger which will convert the event data
-# into a storage blob.
-@app.function_name(name="eventhub_trigger")
-@app.event_hub_message_trigger(arg_name="event",
- event_hub_name="python-worker-ci-eventhub-one",
- connection="AzureWebJobsEventHubConnectionString"
- )
-@app.blob_output(arg_name="$return",
- path="python-worker-tests/test-eventhub-triggered.txt",
- connection="AzureWebJobsStorage")
-def eventhub_trigger(event: func.EventHubEvent) -> bytes:
- return event.get_body()
-
-
-# Retrieve the event data from storage blob and return it as Http response
-@app.function_name(name="get_eventhub_triggered")
-@app.route(route="get_eventhub_triggered")
-@app.blob_input(arg_name="file",
- path="python-worker-tests/test-eventhub-triggered.txt",
- connection="AzureWebJobsStorage")
-def get_eventhub_triggered(req: func.HttpRequest,
- file: func.InputStream) -> str:
- return file.read().decode('utf-8')
-
-
-# Retrieve the event data from storage blob and return it as Http response
-@app.function_name(name="get_metadata_triggered")
-@app.route(route="get_metadata_triggered")
-@app.blob_input(arg_name="file",
- path="python-worker-tests/test-metadata-triggered.txt",
- connection="AzureWebJobsStorage")
-async def get_metadata_triggered(req: func.HttpRequest,
- file: func.InputStream) -> str:
- return func.HttpResponse(body=file.read().decode('utf-8'),
- status_code=200,
- mimetype='application/json')
-
-
-# An HttpTrigger to generating EventHub event from azure-eventhub SDK.
-# Events generated from azure-eventhub contain the full metadata.
-@app.function_name(name="metadata_output")
-@app.route(route="metadata_output")
-async def metadata_output(req: func.HttpRequest):
- # Parse event metadata from http request
- json_string = req.get_body().decode('utf-8')
- event_dict = json.loads(json_string)
-
- # Create an EventHub Client and event batch
- client = EventHubProducerClient.from_connection_string(
- os.getenv('AzureWebJobsEventHubConnectionString'),
- eventhub_name='python-worker-ci-eventhub-one-metadata')
-
- # Generate new event based on http request with full metadata
- event_data_batch = await client.create_batch()
- event_data_batch.add(EventData(event_dict.get('body')))
-
- # Send out event into event hub
- try:
- await client.send_batch(event_data_batch)
- finally:
- await client.close()
-
- return 'OK'
-
-
-@app.function_name(name="metadata_trigger")
-@app.event_hub_message_trigger(
- arg_name="event",
- event_hub_name="python-worker-ci-eventhub-one-metadata",
- connection="AzureWebJobsEventHubConnectionString")
-@app.blob_output(arg_name="$return",
- path="python-worker-tests/test-metadata-triggered.txt",
- connection="AzureWebJobsStorage")
-async def metadata_trigger(event: func.EventHubEvent) -> bytes:
- event_dict: typing.Mapping[str, typing.Any] = {
- 'body': event.get_body().decode('utf-8'),
- # Uncomment this when the EnqueuedTimeUtc is fixed in azure-functions
- # 'enqueued_time': event.enqueued_time.isoformat(),
- 'partition_key': event.partition_key,
- 'sequence_number': event.sequence_number,
- 'offset': event.offset,
- 'metadata': event.metadata
- }
-
- return json.dumps(event_dict)
diff --git a/tests/endtoend/http_v2_functions/fastapi_streaming_download_func/function_app.py b/tests/endtoend/http_v2_functions/fastapi_streaming_download_func/function_app.py
deleted file mode 100644
index 25900aafb..000000000
--- a/tests/endtoend/http_v2_functions/fastapi_streaming_download_func/function_app.py
+++ /dev/null
@@ -1,108 +0,0 @@
-import json
-import os
-import typing
-
-from azure.eventhub import EventData
-from azure.eventhub.aio import EventHubProducerClient
-
-import azure.functions as func
-
-app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
-
-
-# An HttpTrigger to generating EventHub event from EventHub Output Binding
-@app.function_name(name="eventhub_output")
-@app.route(route="eventhub_output")
-@app.event_hub_output(arg_name="event",
- event_hub_name="python-worker-ci-eventhub-one",
- connection="AzureWebJobsEventHubConnectionString")
-def eventhub_output(req: func.HttpRequest, event: func.Out[str]):
- event.set(req.get_body().decode('utf-8'))
- return 'OK'
-
-
-# This is an actual EventHub trigger which will convert the event data
-# into a storage blob.
-@app.function_name(name="eventhub_trigger")
-@app.event_hub_message_trigger(arg_name="event",
- event_hub_name="python-worker-ci-eventhub-one",
- connection="AzureWebJobsEventHubConnectionString"
- )
-@app.blob_output(arg_name="$return",
- path="python-worker-tests/test-eventhub-triggered.txt",
- connection="AzureWebJobsStorage")
-def eventhub_trigger(event: func.EventHubEvent) -> bytes:
- return event.get_body()
-
-
-# Retrieve the event data from storage blob and return it as Http response
-@app.function_name(name="get_eventhub_triggered")
-@app.route(route="get_eventhub_triggered")
-@app.blob_input(arg_name="file",
- path="python-worker-tests/test-eventhub-triggered.txt",
- connection="AzureWebJobsStorage")
-def get_eventhub_triggered(req: func.HttpRequest,
- file: func.InputStream) -> str:
- return file.read().decode('utf-8')
-
-
-# Retrieve the event data from storage blob and return it as Http response
-@app.function_name(name="get_metadata_triggered")
-@app.route(route="get_metadata_triggered")
-@app.blob_input(arg_name="file",
- path="python-worker-tests/test-metadata-triggered.txt",
- connection="AzureWebJobsStorage")
-async def get_metadata_triggered(req: func.HttpRequest,
- file: func.InputStream) -> str:
- return func.HttpResponse(body=file.read().decode('utf-8'),
- status_code=200,
- mimetype='application/json')
-
-
-# An HttpTrigger to generating EventHub event from azure-eventhub SDK.
-# Events generated from azure-eventhub contain the full metadata.
-@app.function_name(name="metadata_output")
-@app.route(route="metadata_output")
-async def metadata_output(req: func.HttpRequest):
- # Parse event metadata from http request
- json_string = req.get_body().decode('utf-8')
- event_dict = json.loads(json_string)
-
- # Create an EventHub Client and event batch
- client = EventHubProducerClient.from_connection_string(
- os.getenv('AzureWebJobsEventHubConnectionString'),
- eventhub_name='python-worker-ci-eventhub-one-metadata')
-
- # Generate new event based on http request with full metadata
- event_data_batch = await client.create_batch()
- event_data_batch.add(EventData(event_dict.get('body')))
-
- # Send out event into event hub
- try:
- await client.send_batch(event_data_batch)
- finally:
- await client.close()
-
- return 'OK'
-
-
-@app.function_name(name="metadata_trigger")
-@app.event_hub_message_trigger(
- arg_name="event",
- event_hub_name="python-worker-ci-eventhub-one-metadata",
- connection="AzureWebJobsEventHubConnectionString")
-@app.blob_output(arg_name="$return",
- path="python-worker-tests/test-metadata-triggered.txt",
- connection="AzureWebJobsStorage")
-async def metadata_trigger(event: func.EventHubEvent) -> bytes:
- event_dict: typing.Mapping[str, typing.Any] = {
- 'body': event.get_body().decode('utf-8'),
- # Uncomment this when the EnqueuedTimeUtc is fixed in azure-functions
- # 'enqueued_time': event.enqueued_time.isoformat(),
- 'partition_key': event.partition_key,
- 'sequence_number': event.sequence_number,
- 'offset': event.offset,
- 'metadata': event.metadata
- }
-
- return json.dumps(event_dict)
diff --git a/tests/endtoend/http_v2_functions/fastapi_streaming_upload_func/function_app.py b/tests/endtoend/http_v2_functions/fastapi_streaming_upload_func/function_app.py
deleted file mode 100644
index 25900aafb..000000000
--- a/tests/endtoend/http_v2_functions/fastapi_streaming_upload_func/function_app.py
+++ /dev/null
@@ -1,108 +0,0 @@
-import json
-import os
-import typing
-
-from azure.eventhub import EventData
-from azure.eventhub.aio import EventHubProducerClient
-
-import azure.functions as func
-
-app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
-
-
-# An HttpTrigger to generating EventHub event from EventHub Output Binding
-@app.function_name(name="eventhub_output")
-@app.route(route="eventhub_output")
-@app.event_hub_output(arg_name="event",
- event_hub_name="python-worker-ci-eventhub-one",
- connection="AzureWebJobsEventHubConnectionString")
-def eventhub_output(req: func.HttpRequest, event: func.Out[str]):
- event.set(req.get_body().decode('utf-8'))
- return 'OK'
-
-
-# This is an actual EventHub trigger which will convert the event data
-# into a storage blob.
-@app.function_name(name="eventhub_trigger")
-@app.event_hub_message_trigger(arg_name="event",
- event_hub_name="python-worker-ci-eventhub-one",
- connection="AzureWebJobsEventHubConnectionString"
- )
-@app.blob_output(arg_name="$return",
- path="python-worker-tests/test-eventhub-triggered.txt",
- connection="AzureWebJobsStorage")
-def eventhub_trigger(event: func.EventHubEvent) -> bytes:
- return event.get_body()
-
-
-# Retrieve the event data from storage blob and return it as Http response
-@app.function_name(name="get_eventhub_triggered")
-@app.route(route="get_eventhub_triggered")
-@app.blob_input(arg_name="file",
- path="python-worker-tests/test-eventhub-triggered.txt",
- connection="AzureWebJobsStorage")
-def get_eventhub_triggered(req: func.HttpRequest,
- file: func.InputStream) -> str:
- return file.read().decode('utf-8')
-
-
-# Retrieve the event data from storage blob and return it as Http response
-@app.function_name(name="get_metadata_triggered")
-@app.route(route="get_metadata_triggered")
-@app.blob_input(arg_name="file",
- path="python-worker-tests/test-metadata-triggered.txt",
- connection="AzureWebJobsStorage")
-async def get_metadata_triggered(req: func.HttpRequest,
- file: func.InputStream) -> str:
- return func.HttpResponse(body=file.read().decode('utf-8'),
- status_code=200,
- mimetype='application/json')
-
-
-# An HttpTrigger to generating EventHub event from azure-eventhub SDK.
-# Events generated from azure-eventhub contain the full metadata.
-@app.function_name(name="metadata_output")
-@app.route(route="metadata_output")
-async def metadata_output(req: func.HttpRequest):
- # Parse event metadata from http request
- json_string = req.get_body().decode('utf-8')
- event_dict = json.loads(json_string)
-
- # Create an EventHub Client and event batch
- client = EventHubProducerClient.from_connection_string(
- os.getenv('AzureWebJobsEventHubConnectionString'),
- eventhub_name='python-worker-ci-eventhub-one-metadata')
-
- # Generate new event based on http request with full metadata
- event_data_batch = await client.create_batch()
- event_data_batch.add(EventData(event_dict.get('body')))
-
- # Send out event into event hub
- try:
- await client.send_batch(event_data_batch)
- finally:
- await client.close()
-
- return 'OK'
-
-
-@app.function_name(name="metadata_trigger")
-@app.event_hub_message_trigger(
- arg_name="event",
- event_hub_name="python-worker-ci-eventhub-one-metadata",
- connection="AzureWebJobsEventHubConnectionString")
-@app.blob_output(arg_name="$return",
- path="python-worker-tests/test-metadata-triggered.txt",
- connection="AzureWebJobsStorage")
-async def metadata_trigger(event: func.EventHubEvent) -> bytes:
- event_dict: typing.Mapping[str, typing.Any] = {
- 'body': event.get_body().decode('utf-8'),
- # Uncomment this when the EnqueuedTimeUtc is fixed in azure-functions
- # 'enqueued_time': event.enqueued_time.isoformat(),
- 'partition_key': event.partition_key,
- 'sequence_number': event.sequence_number,
- 'offset': event.offset,
- 'metadata': event.metadata
- }
-
- return json.dumps(event_dict)
diff --git a/tests/endtoend/test_http_functions.py b/tests/endtoend/test_http_functions.py
index 9d6f9fdf6..8d7061878 100644
--- a/tests/endtoend/test_http_functions.py
+++ b/tests/endtoend/test_http_functions.py
@@ -1,7 +1,9 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
+import concurrent
import os
import typing
+from concurrent.futures import ThreadPoolExecutor
from unittest.mock import patch
import requests
@@ -220,19 +222,157 @@ def tearDownClass(cls):
super().tearDownClass()
-class TestHttpFunctionsV2WithInitIndexing(TestHttpFunctionsStein):
-
- @classmethod
- def setUpClass(cls):
- os.environ[PYTHON_ENABLE_INIT_INDEXING] = "1"
- super().setUpClass()
-
- @classmethod
- def tearDownClass(cls):
- # Remove the PYTHON_SCRIPT_FILE_NAME environment variable
- os.environ.pop(PYTHON_ENABLE_INIT_INDEXING)
- super().tearDownClass()
-
+class TestHttpFunctionsV2FastApiWithInitIndexing(TestHttpFunctionsWithInitIndexing):
+ @classmethod
+ def get_script_dir(cls):
+ return testutils.E2E_TESTS_FOLDER / 'http_functions' / \
+ 'http_functions_v2' / \
+ 'fastapi'
+
+ @testutils.retryable_test(3, 5)
+ def test_return_streaming(self):
+ """Test if the return_streaming function returns a streaming
+ response"""
+ root_url = self.webhost._addr
+ streaming_url = f'{root_url}/api/return_streaming'
+ r = requests.get(streaming_url, timeout=REQUEST_TIMEOUT_SEC, stream=True)
+ self.assertTrue(r.ok)
+ # Validate streaming content
+ expected_content = [b"First chunk\n", b"Second chunk\n"]
+ received_content = []
+ for chunk in r.iter_content(chunk_size=1024):
+ if chunk:
+ received_content.append(chunk)
+ self.assertEqual(received_content, expected_content)
+
+ @testutils.retryable_test(3, 5)
+ def test_return_streaming_concurrently(self):
+ """Test if the return_streaming function returns a streaming
+ response concurrently"""
+ root_url = self.webhost._addr
+ streaming_url = f'{root_url}/return_streaming'
+
+ # Function to make a streaming request and validate content
+ def make_request():
+ r = requests.get(streaming_url, timeout=REQUEST_TIMEOUT_SEC,
+ stream=True)
+ self.assertTrue(r.ok)
+ expected_content = [b"First chunk\n", b"Second chunk\n"]
+ received_content = []
+ for chunk in r.iter_content(chunk_size=1024):
+ if chunk:
+ received_content.append(chunk)
+ self.assertEqual(received_content, expected_content)
+
+ # Make concurrent requests
+ with ThreadPoolExecutor(max_workers=2) as executor:
+ executor.map(make_request, range(2))
+
+ @testutils.retryable_test(3, 5)
+ def test_return_html(self):
+ """Test if the return_html function returns an HTML response"""
+ root_url = self.webhost._addr
+ html_url = f'{root_url}/api/return_html'
+ r = requests.get(html_url, timeout=REQUEST_TIMEOUT_SEC)
+ self.assertTrue(r.ok)
+ self.assertEqual(r.headers['content-type'],
+ 'text/html; charset=utf-8')
+ # Validate HTML content
+ expected_html = "Hello, World!
"
+ self.assertEqual(r.text, expected_html)
+
+ @testutils.retryable_test(3, 5)
+ def test_return_ujson(self):
+ """Test if the return_ujson function returns a UJSON response"""
+ root_url = self.webhost._addr
+ ujson_url = f'{root_url}/api/return_ujson'
+ r = requests.get(ujson_url, timeout=REQUEST_TIMEOUT_SEC)
+ self.assertTrue(r.ok)
+ self.assertEqual(r.headers['content-type'],'application/json')
+ self.assertEqual(r.text, '{"message":"Hello, World!"}')
+
+ @testutils.retryable_test(3, 5)
+ def test_return_orjson(self):
+ """Test if the return_orjson function returns an ORJSON response"""
+ root_url = self.webhost._addr
+ orjson_url = f'{root_url}/api/return_orjson'
+ r = requests.get(orjson_url, timeout=REQUEST_TIMEOUT_SEC)
+ self.assertTrue(r.ok)
+ self.assertEqual(r.headers['content-type'], 'application/json')
+ self.assertEqual(r.text, '{"message":"Hello, World!"}')
+
+ @testutils.retryable_test(3, 5)
+ def test_return_file(self):
+ """Test if the return_file function returns a file response"""
+ root_url = self.webhost._addr
+ file_url = f'{root_url}/api/return_file'
+ r = requests.get(file_url, timeout=REQUEST_TIMEOUT_SEC)
+ self.assertTrue(r.ok)
+ self.assertIn('@app.route(route="default_template")', r.text)
+
+ @testutils.retryable_test(3, 5)
+ def test_upload_data_stream(self):
+ """Test if the upload_data_stream function receives streaming data
+ and returns the complete data"""
+ root_url = self.webhost._addr
+ upload_url = f'{root_url}/api/upload_data_stream'
+
+ # Define the streaming data
+ data_chunks = [b"First chunk\n", b"Second chunk\n"]
+
+ # Define a function to simulate streaming by reading from an
+ # iterator
+ def stream_data(data_chunks):
+ for chunk in data_chunks:
+ yield chunk
+
+ # Send a POST request with streaming data
+ r = requests.post(upload_url, data=stream_data(data_chunks))
+
+ # Assert that the request was successful
+ self.assertTrue(r.ok)
+
+ # Assert that the response content matches the concatenation of
+ # all data chunks
+ complete_data = b"".join(data_chunks)
+ self.assertEqual(r.content, complete_data)
+
+ @testutils.retryable_test(3, 5)
+ def test_upload_data_stream_concurrently(self):
+ """Test if the upload_data_stream function receives streaming data
+ and returns the complete data"""
+ root_url = self.webhost._addr
+ upload_url = f'{root_url}/api/upload_data_stream'
+
+ # Define the streaming data
+ data_chunks = [b"First chunk\n", b"Second chunk\n"]
+
+ # Define a function to simulate streaming by reading from an
+ # iterator
+ def stream_data(data_chunks):
+ for chunk in data_chunks:
+ yield chunk
+
+ # Define the number of concurrent requests
+ num_requests = 5
+
+ # Define a function to send a single request
+ def send_request():
+ r = requests.post(upload_url, data=stream_data(data_chunks))
+ return r.ok, r.content
+
+ # Send multiple requests concurrently
+ with concurrent.futures.ThreadPoolExecutor() as executor:
+ futures = [executor.submit(send_request) for _ in
+ range(num_requests)]
+
+ # Assert that all requests were successful and the response
+ # contents are correct
+ for future in concurrent.futures.as_completed(futures):
+ ok, content = future.result()
+ self.assertTrue(ok)
+ complete_data = b"".join(data_chunks)
+ self.assertEqual(content, complete_data)
class TestUserThreadLoggingHttpFunctions(testutils.WebHostTestCase):
"""Test the Http trigger that contains logging with user threads.
diff --git a/tests/unittests/http_functions/http_v2_functions/fastapi/function_app.py b/tests/unittests/http_functions/http_v2_functions/fastapi/function_app.py
new file mode 100644
index 000000000..25accf853
--- /dev/null
+++ b/tests/unittests/http_functions/http_v2_functions/fastapi/function_app.py
@@ -0,0 +1,434 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+import asyncio
+import hashlib
+import json
+import logging
+import sys
+import time
+from urllib.request import urlopen
+from azure.functions.extension.fastapi import Request, Response, \
+ PlainTextResponse, HTMLResponse, RedirectResponse
+import azure.functions as func
+from pydantic import BaseModel
+
+app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
+
+logger = logging.getLogger("my-function")
+
+
+class Item(BaseModel):
+ name: str
+ description: str
+
+
+@app.route(route="no_type_hint")
+def no_type_hint(req):
+ return 'no_type_hint'
+
+
+@app.route(route="return_int")
+def return_int(req) -> int:
+ return 1000
+
+
+@app.route(route="return_float")
+def return_float(req) -> float:
+ return 1000.0
+
+
+@app.route(route="return_bool")
+def return_bool(req) -> bool:
+ return True
+
+
+@app.route(route="return_dict")
+def return_dict(req) -> dict:
+ return {"key": "value"}
+
+
+@app.route(route="return_list")
+def return_list(req):
+ return ["value1", "value2"]
+
+
+@app.route(route="return_pydantic_model")
+def return_pydantic_model(req) -> Item:
+ item = Item(name="item1", description="description1")
+ return item
+
+
+@app.route(route="return_pydantic_model_with_missing_fields")
+def return_pydantic_model_with_missing_fields(req) -> Item:
+ item = Item(name="item1")
+ return item
+
+
+@app.route(route="accept_json")
+async def accept_json(req: Request):
+ return await req.json()
+
+
+async def nested():
+ try:
+ 1 / 0
+ except ZeroDivisionError:
+ logger.error('and another error', exc_info=True)
+
+
+@app.route(route="async_logging")
+async def async_logging(req: Request):
+ logger.info('hello %s', 'info')
+
+ await asyncio.sleep(0.1)
+
+ # Create a nested task to check if invocation_id is still
+ # logged correctly.
+ await asyncio.ensure_future(nested())
+
+ await asyncio.sleep(0.1)
+
+ return 'OK-async'
+
+
+@app.route(route="async_return_str")
+async def async_return_str(req: Request):
+ await asyncio.sleep(0.1)
+ return 'Hello Async World!'
+
+
+@app.route(route="debug_logging")
+def debug_logging(req: Request):
+ logging.critical('logging critical', exc_info=True)
+ logging.info('logging info', exc_info=True)
+ logging.warning('logging warning', exc_info=True)
+ logging.debug('logging debug', exc_info=True)
+ logging.error('logging error', exc_info=True)
+ return 'OK-debug'
+
+
+@app.route(route="debug_user_logging")
+def debug_user_logging(req: Request):
+ logger.setLevel(logging.DEBUG)
+
+ logging.critical('logging critical', exc_info=True)
+ logger.info('logging info', exc_info=True)
+ logger.warning('logging warning', exc_info=True)
+ logger.debug('logging debug', exc_info=True)
+ logger.error('logging error', exc_info=True)
+ return 'OK-user-debug'
+
+
+# Attempt to log info into system log from customer code
+disguised_logger = logging.getLogger('azure_functions_worker')
+
+
+async def parallelly_print():
+ await asyncio.sleep(0.1)
+ print('parallelly_print')
+
+
+async def parallelly_log_info():
+ await asyncio.sleep(0.2)
+ logging.info('parallelly_log_info at root logger')
+
+
+async def parallelly_log_warning():
+ await asyncio.sleep(0.3)
+ logging.warning('parallelly_log_warning at root logger')
+
+
+async def parallelly_log_error():
+ await asyncio.sleep(0.4)
+ logging.error('parallelly_log_error at root logger')
+
+
+async def parallelly_log_exception():
+ await asyncio.sleep(0.5)
+ try:
+ raise Exception('custom exception')
+ except Exception:
+ logging.exception('parallelly_log_exception at root logger',
+ exc_info=sys.exc_info())
+
+
+async def parallelly_log_custom():
+ await asyncio.sleep(0.6)
+ logger.info('parallelly_log_custom at custom_logger')
+
+
+async def parallelly_log_system():
+ await asyncio.sleep(0.7)
+ disguised_logger.info('parallelly_log_system at disguised_logger')
+
+
+@app.route(route="hijack_current_event_loop")
+async def hijack_current_event_loop(req: Request) -> Response:
+ loop = asyncio.get_event_loop()
+
+ # Create multiple tasks and schedule it into one asyncio.wait blocker
+ task_print: asyncio.Task = loop.create_task(parallelly_print())
+ task_info: asyncio.Task = loop.create_task(parallelly_log_info())
+ task_warning: asyncio.Task = loop.create_task(parallelly_log_warning())
+ task_error: asyncio.Task = loop.create_task(parallelly_log_error())
+ task_exception: asyncio.Task = loop.create_task(parallelly_log_exception())
+ task_custom: asyncio.Task = loop.create_task(parallelly_log_custom())
+ task_disguise: asyncio.Task = loop.create_task(parallelly_log_system())
+
+ # Create an awaitable future and occupy the current event loop resource
+ future = loop.create_future()
+ loop.call_soon_threadsafe(future.set_result, 'callsoon_log')
+
+ # WaitAll
+ await asyncio.wait([task_print, task_info, task_warning, task_error,
+ task_exception, task_custom, task_disguise, future])
+
+ # Log asyncio low-level future result
+ logging.info(future.result())
+
+ return 'OK-hijack-current-event-loop'
+
+
+@app.route(route="print_logging")
+def print_logging(req: Request):
+ flush_required = False
+ is_console_log = False
+ is_stderr = False
+ message = req.query_params.get('message', '')
+
+ if req.query_params.get('flush') == 'true':
+ flush_required = True
+ if req.query_params.get('console') == 'true':
+ is_console_log = True
+ if req.query_params.get('is_stderr') == 'true':
+ is_stderr = True
+
+ # Adding LanguageWorkerConsoleLog will make function host to treat
+ # this as system log and will be propagated to kusto
+ prefix = 'LanguageWorkerConsoleLog' if is_console_log else ''
+ print(f'{prefix} {message}'.strip(),
+ file=sys.stderr if is_stderr else sys.stdout,
+ flush=flush_required)
+
+ return 'OK-print-logging'
+
+
+@app.route(route="raw_body_bytes")
+async def raw_body_bytes(req: Request) -> Response:
+ body = await req.body()
+ body_len = str(len(body))
+
+ headers = {'body-len': body_len}
+ return Response(content=body, status_code=200, headers=headers)
+
+
+@app.route(route="remapped_context")
+def remapped_context(req: Request):
+ return req.method
+
+
+@app.route(route="return_bytes")
+def return_bytes(req: Request):
+ return b"Hello World"
+
+
+@app.route(route="return_context")
+def return_context(req: Request, context: func.Context):
+ return {
+ 'method': req.method,
+ 'ctx_func_name': context.function_name,
+ 'ctx_func_dir': context.function_directory,
+ 'ctx_invocation_id': context.invocation_id,
+ 'ctx_trace_context_Traceparent': context.trace_context.Traceparent,
+ 'ctx_trace_context_Tracestate': context.trace_context.Tracestate,
+ }
+
+
+@app.route(route="return_http")
+def return_http(req: Request) -> HTMLResponse:
+ return HTMLResponse('Hello World™
')
+
+
+@app.route(route="return_http_404")
+def return_http_404(req: Request):
+ return Response('bye', status_code=404)
+
+
+@app.route(route="return_http_auth_admin", auth_level=func.AuthLevel.ADMIN)
+def return_http_auth_admin(req: Request) -> HTMLResponse:
+ return HTMLResponse('Hello World™
')
+
+
+@app.route(route="return_http_no_body")
+def return_http_no_body(req: Request):
+ return Response()
+
+
+@app.route(route="return_http_redirect")
+def return_http_redirect(req: Request):
+ return RedirectResponse(url='/api/return_http', status_code=302)
+
+
+@app.route(route="return_request")
+async def return_request(req: Request):
+ params = dict(req.query_params)
+ params.pop('code', None) # Remove 'code' parameter if present
+
+ # Get the body content and calculate its hash
+ body = await req.body()
+ body_hash = hashlib.sha256(body).hexdigest() if body else None
+
+ # Return a dictionary containing request information
+ return {
+ 'method': req.method,
+ 'url': str(req.url),
+ 'headers': dict(req.headers),
+ 'params': params,
+ 'body': body.decode() if body else None,
+ 'body_hash': body_hash,
+ }
+
+
+@app.route(route="return_route_params/{param1}/{param2}")
+def return_route_params(req: Request) -> str:
+ # log type of req
+ logger.info(f"req type: {type(req)}")
+ # log req path params
+ logger.info(f"req path params: {req.path_params}")
+ return req.path_params
+
+
+@app.route(route="sync_logging")
+def main(req: Request):
+ try:
+ 1 / 0
+ except ZeroDivisionError:
+ logger.error('a gracefully handled error', exc_info=True)
+ logger.error('a gracefully handled critical error', exc_info=True)
+ time.sleep(0.05)
+ return 'OK-sync'
+
+
+@app.route(route="unhandled_error")
+def unhandled_error(req: Request):
+ 1 / 0
+
+
+@app.route(route="unhandled_urllib_error")
+def unhandled_urllib_error(req: Request) -> str:
+ image_url = req.params.get('img')
+ urlopen(image_url).read()
+
+
+class UnserializableException(Exception):
+ def __str__(self):
+ raise RuntimeError('cannot serialize me')
+
+
+@app.route(route="unhandled_unserializable_error")
+def unhandled_unserializable_error(req: Request) -> str:
+ raise UnserializableException('foo')
+
+
+async def try_log():
+ logger.info("try_log")
+
+
+@app.route(route="user_event_loop")
+def user_event_loop(req: Request) -> Response:
+ loop = asyncio.SelectorEventLoop()
+ asyncio.set_event_loop(loop)
+
+ # This line should throws an asyncio RuntimeError exception
+ loop.run_until_complete(try_log())
+ loop.close()
+ return 'OK-user-event-loop'
+
+
+@app.route(route="multiple_set_cookie_resp_headers")
+async def multiple_set_cookie_resp_headers(req: Request):
+ logging.info('Python HTTP trigger function processed a request.')
+ resp = Response(
+ "This HTTP triggered function executed successfully.")
+
+ expires_1 = "Thu, 12 Jan 2017 13:55:08 GMT"
+ expires_2 = "Fri, 12 Jan 2018 13:55:08 GMT"
+
+ resp.set_cookie(
+ key='foo3',
+ value='42',
+ domain='example.com',
+ expires=expires_1,
+ path='/',
+ max_age=10000000,
+ secure=True,
+ httponly=True,
+ samesite='Lax'
+ )
+
+ resp.set_cookie(
+ key='foo3',
+ value='43',
+ domain='example.com',
+ expires=expires_2,
+ path='/',
+ max_age=10000000,
+ secure=True,
+ httponly=True,
+ samesite='Lax'
+ )
+
+ return resp
+
+
+@app.route(route="response_cookie_header_nullable_bool_err")
+def response_cookie_header_nullable_bool_err(
+ req: Request) -> Response:
+ logging.info('Python HTTP trigger function processed a request.')
+ resp = Response(
+ "This HTTP triggered function executed successfully.")
+
+ # Set the cookie with Secure attribute set to False
+ resp.set_cookie(
+ key='foo3',
+ value='42',
+ domain='example.com',
+ expires='Thu, 12-Jan-2017 13:55:08 GMT',
+ path='/',
+ max_age=10000000,
+ secure=False,
+ httponly=True
+ )
+
+ return resp
+
+
+@app.route(route="response_cookie_header_nullable_timestamp_err")
+def response_cookie_header_nullable_timestamp_err(
+ req: Request) -> Response:
+ logging.info('Python HTTP trigger function processed a request.')
+ resp = Response(
+ "This HTTP triggered function executed successfully.")
+
+ resp.set_cookie(
+ key='foo3',
+ value='42',
+ domain='example.com'
+ )
+
+ return resp
+
+
+@app.route(route="set_cookie_resp_header_default_values")
+def set_cookie_resp_header_default_values(
+ req: Request) -> Response:
+ logging.info('Python HTTP trigger function processed a request.')
+ resp = Response(
+ "This HTTP triggered function executed successfully.")
+
+ resp.set_cookie(
+ key='foo3',
+ value='42'
+ )
+
+ return resp
\ No newline at end of file
diff --git a/tests/unittests/http_functions/http_v2_functions/function_app.py b/tests/unittests/http_functions/http_v2_functions/function_app.py
deleted file mode 100644
index f4bcfb36e..000000000
--- a/tests/unittests/http_functions/http_v2_functions/function_app.py
+++ /dev/null
@@ -1,419 +0,0 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-import asyncio
-import hashlib
-import json
-import logging
-import sys
-import time
-from urllib.request import urlopen
-from azure.functions.extension.fastapi import Request, Response
-import azure.functions as func
-
-app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
-
-logger = logging.getLogger("my-function")
-
-# request handling
-# request body, query, headers, and route params
-# request validation errors
-# diff http verbs
-
-# response handling
-# response body, status code, headers
-# error responses
-
-# edge cases
-# invalid requests sent behavior with missing body, query, headers, and route params
-# request payload exceeds max size
-# request payload contains special characters
-
-@app.route(route="return_str")
-def return_str(req: Request) -> str:
- return 'Hello World!'
-
-#
-# @app.route(route="accept_json")
-# def accept_json(req: Request):
-# return json.dumps({
-# 'method': req.method,
-# 'url': req.url,
-# 'headers': dict(req.headers),
-# 'params': dict(req.params),
-# 'get_body': req.get_body().decode(),
-# 'get_json': req.get_json()
-# })
-#
-#
-# async def nested():
-# try:
-# 1 / 0
-# except ZeroDivisionError:
-# logger.error('and another error', exc_info=True)
-#
-#
-# @app.route(route="async_logging")
-# async def async_logging(req: Request):
-# logger.info('hello %s', 'info')
-#
-# await asyncio.sleep(0.1)
-#
-# # Create a nested task to check if invocation_id is still
-# # logged correctly.
-# await asyncio.ensure_future(nested())
-#
-# await asyncio.sleep(0.1)
-#
-# return 'OK-async'
-#
-#
-# @app.route(route="async_return_str")
-# async def async_return_str(req: Request):
-# await asyncio.sleep(0.1)
-# return 'Hello Async World!'
-#
-#
-# @app.route(route="debug_logging")
-# def debug_logging(req: Request):
-# logging.critical('logging critical', exc_info=True)
-# logging.info('logging info', exc_info=True)
-# logging.warning('logging warning', exc_info=True)
-# logging.debug('logging debug', exc_info=True)
-# logging.error('logging error', exc_info=True)
-# return 'OK-debug'
-#
-#
-# @app.route(route="debug_user_logging")
-# def debug_user_logging(req: Request):
-# logger.setLevel(logging.DEBUG)
-#
-# logging.critical('logging critical', exc_info=True)
-# logger.info('logging info', exc_info=True)
-# logger.warning('logging warning', exc_info=True)
-# logger.debug('logging debug', exc_info=True)
-# logger.error('logging error', exc_info=True)
-# return 'OK-user-debug'
-#
-#
-# # Attempt to log info into system log from customer code
-# disguised_logger = logging.getLogger('azure_functions_worker')
-#
-#
-# async def parallelly_print():
-# await asyncio.sleep(0.1)
-# print('parallelly_print')
-#
-#
-# async def parallelly_log_info():
-# await asyncio.sleep(0.2)
-# logging.info('parallelly_log_info at root logger')
-#
-#
-# async def parallelly_log_warning():
-# await asyncio.sleep(0.3)
-# logging.warning('parallelly_log_warning at root logger')
-#
-#
-# async def parallelly_log_error():
-# await asyncio.sleep(0.4)
-# logging.error('parallelly_log_error at root logger')
-#
-#
-# async def parallelly_log_exception():
-# await asyncio.sleep(0.5)
-# try:
-# raise Exception('custom exception')
-# except Exception:
-# logging.exception('parallelly_log_exception at root logger',
-# exc_info=sys.exc_info())
-#
-#
-# async def parallelly_log_custom():
-# await asyncio.sleep(0.6)
-# logger.info('parallelly_log_custom at custom_logger')
-#
-#
-# async def parallelly_log_system():
-# await asyncio.sleep(0.7)
-# disguised_logger.info('parallelly_log_system at disguised_logger')
-#
-#
-# @app.route(route="hijack_current_event_loop")
-# async def hijack_current_event_loop(req: Request) -> Response:
-# loop = asyncio.get_event_loop()
-#
-# # Create multiple tasks and schedule it into one asyncio.wait blocker
-# task_print: asyncio.Task = loop.create_task(parallelly_print())
-# task_info: asyncio.Task = loop.create_task(parallelly_log_info())
-# task_warning: asyncio.Task = loop.create_task(parallelly_log_warning())
-# task_error: asyncio.Task = loop.create_task(parallelly_log_error())
-# task_exception: asyncio.Task = loop.create_task(parallelly_log_exception())
-# task_custom: asyncio.Task = loop.create_task(parallelly_log_custom())
-# task_disguise: asyncio.Task = loop.create_task(parallelly_log_system())
-#
-# # Create an awaitable future and occupy the current event loop resource
-# future = loop.create_future()
-# loop.call_soon_threadsafe(future.set_result, 'callsoon_log')
-#
-# # WaitAll
-# await asyncio.wait([task_print, task_info, task_warning, task_error,
-# task_exception, task_custom, task_disguise, future])
-#
-# # Log asyncio low-level future result
-# logging.info(future.result())
-#
-# return 'OK-hijack-current-event-loop'
-#
-#
-# @app.route(route="no_return")
-# def no_return(req: Request):
-# logger.info('hi')
-#
-#
-# @app.route(route="no_return_returns")
-# def no_return_returns(req):
-# return 'ABC'
-#
-#
-# @app.route(route="print_logging")
-# def print_logging(req: Request):
-# flush_required = False
-# is_console_log = False
-# is_stderr = False
-# message = req.params.get('message', '')
-#
-# if req.params.get('flush') == 'true':
-# flush_required = True
-# if req.params.get('console') == 'true':
-# is_console_log = True
-# if req.params.get('is_stderr') == 'true':
-# is_stderr = True
-#
-# # Adding LanguageWorkerConsoleLog will make function host to treat
-# # this as system log and will be propagated to kusto
-# prefix = 'LanguageWorkerConsoleLog' if is_console_log else ''
-# print(f'{prefix} {message}'.strip(),
-# file=sys.stderr if is_stderr else sys.stdout,
-# flush=flush_required)
-#
-# return 'OK-print-logging'
-#
-#
-# @app.route(route="raw_body_bytes")
-# def raw_body_bytes(req: Request) -> Response:
-# body = req.get_body()
-# body_len = str(len(body))
-#
-# headers = {'body-len': body_len}
-# return Response(body=body, status_code=200, headers=headers)
-#
-#
-# @app.route(route="remapped_context")
-# def remapped_context(req: Request):
-# return req.method
-#
-#
-# @app.route(route="return_bytes")
-# def return_bytes(req: Request):
-# # This function will fail, as we don't auto-convert "bytes" to "http".
-# return b'Hello World!'
-#
-#
-# @app.route(route="return_context")
-# def return_context(req: Request, context: func.Context):
-# return json.dumps({
-# 'method': req.method,
-# 'ctx_func_name': context.function_name,
-# 'ctx_func_dir': context.function_directory,
-# 'ctx_invocation_id': context.invocation_id,
-# 'ctx_trace_context_Traceparent': context.trace_context.Traceparent,
-# 'ctx_trace_context_Tracestate': context.trace_context.Tracestate,
-# })
-#
-#
-# @app.route(route="return_http")
-# def return_http(req: Request):
-# return Response('Hello World™
',
-# mimetype='text/html')
-#
-#
-# @app.route(route="return_http_404")
-# def return_http_404(req: Request):
-# return Response('bye', status_code=404)
-#
-#
-# @app.route(route="return_http_auth_admin", auth_level=func.AuthLevel.ADMIN)
-# def return_http_auth_admin(req: Request):
-# return Response('Hello World™
',
-# mimetype='text/html')
-#
-#
-# @app.route(route="return_http_no_body")
-# def return_http_no_body(req: Request):
-# return Response()
-#
-#
-# @app.route(route="return_http_redirect")
-# def return_http_redirect(req: Request):
-# location = 'return_http?code={}'.format(req.params['code'])
-# return Response(
-# status_code=302,
-# headers={'location': location})
-#
-#
-# @app.route(route="return_out", binding_arg_name="foo")
-# def return_out(req: Request, foo: func.Out[Response]):
-# foo.set(Response(body='hello', status_code=201))
-#
-#
-# @app.route(route="return_request")
-# def return_request(req: Request):
-# params = dict(req.params)
-# params.pop('code', None)
-# body = req.get_body()
-# return json.dumps({
-# 'method': req.method,
-# 'url': req.url,
-# 'headers': dict(req.headers),
-# 'params': params,
-# 'get_body': body.decode(),
-# 'body_hash': hashlib.sha256(body).hexdigest(),
-# })
-#
-#
-# @app.route(route="return_route_params/{param1}/{param2}")
-# def return_route_params(req: Request) -> str:
-# return json.dumps(dict(req.route_params))
-#
-#
-# @app.route(route="sync_logging")
-# def main(req: Request):
-# try:
-# 1 / 0
-# except ZeroDivisionError:
-# logger.error('a gracefully handled error', exc_info=True)
-# logger.error('a gracefully handled critical error', exc_info=True)
-# time.sleep(0.05)
-# return 'OK-sync'
-#
-#
-# @app.route(route="unhandled_error")
-# def unhandled_error(req: Request):
-# 1 / 0
-#
-#
-# @app.route(route="unhandled_urllib_error")
-# def unhandled_urllib_error(req: Request) -> str:
-# image_url = req.params.get('img')
-# urlopen(image_url).read()
-#
-#
-# class UnserializableException(Exception):
-# def __str__(self):
-# raise RuntimeError('cannot serialize me')
-#
-#
-# @app.route(route="unhandled_unserializable_error")
-# def unhandled_unserializable_error(req: Request) -> str:
-# raise UnserializableException('foo')
-#
-#
-# async def try_log():
-# logger.info("try_log")
-#
-#
-# @app.route(route="user_event_loop")
-# def user_event_loop(req: Request) -> Response:
-# loop = asyncio.SelectorEventLoop()
-# asyncio.set_event_loop(loop)
-#
-# # This line should throws an asyncio RuntimeError exception
-# loop.run_until_complete(try_log())
-# loop.close()
-# return 'OK-user-event-loop'
-#
-#
-# @app.route(route="multiple_set_cookie_resp_headers")
-# def multiple_set_cookie_resp_headers(
-# req: Request) -> Response:
-# logging.info('Python HTTP trigger function processed a request.')
-# resp = Response(
-# "This HTTP triggered function executed successfully.")
-#
-# resp.headers.add("Set-Cookie",
-# 'foo3=42; Domain=example.com; Expires=Thu, 12-Jan-2017 '
-# '13:55:08 GMT; Path=/; Max-Age=10000000; Secure; '
-# 'HttpOnly')
-# resp.headers.add("Set-Cookie",
-# 'foo3=43; Domain=example.com; Expires=Thu, 12-Jan-2018 '
-# '13:55:08 GMT; Path=/; Max-Age=10000000; Secure; '
-# 'HttpOnly')
-# resp.headers.add("HELLO", 'world')
-#
-# return resp
-#
-#
-# @app.route(route="response_cookie_header_nullable_bool_err")
-# def response_cookie_header_nullable_bool_err(
-# req: Request) -> Response:
-# logging.info('Python HTTP trigger function processed a request.')
-# resp = Response(
-# "This HTTP triggered function executed successfully.")
-#
-# resp.headers.add("Set-Cookie",
-# 'foo3=42; Domain=example.com; Expires=Thu, 12-Jan-2017 '
-# '13:55:08 GMT; Path=/; Max-Age=10000000; SecureFalse; '
-# 'HttpOnly')
-#
-# return resp
-#
-#
-# @app.route(route="response_cookie_header_nullable_double_err")
-# def response_cookie_header_nullable_double_err(
-# req: Request) -> Response:
-# logging.info('Python HTTP trigger function processed a request.')
-# resp = Response(
-# "This HTTP triggered function executed successfully.")
-#
-# resp.headers.add("Set-Cookie",
-# 'foo3=42; Domain=example.com; Expires=Thu, 12-Jan-2017 '
-# '13:55:08 GMT; Path=/; Max-Age=Dummy; SecureFalse; '
-# 'HttpOnly')
-#
-# return resp
-#
-#
-# @app.route(route="response_cookie_header_nullable_timestamp_err")
-# def response_cookie_header_nullable_timestamp_err(
-# req: Request) -> Response:
-# logging.info('Python HTTP trigger function processed a request.')
-# resp = Response(
-# "This HTTP triggered function executed successfully.")
-#
-# resp.headers.add("Set-Cookie", 'foo=bar; Domain=123; Expires=Dummy')
-#
-# return resp
-#
-#
-# @app.route(route="set_cookie_resp_header_default_values")
-# def set_cookie_resp_header_default_values(
-# req: Request) -> Response:
-# logging.info('Python HTTP trigger function processed a request.')
-# resp = Response(
-# "This HTTP triggered function executed successfully.")
-#
-# resp.headers.add("Set-Cookie", 'foo=bar')
-#
-# return resp
-#
-#
-# @app.route(route="set_cookie_resp_header_empty")
-# def set_cookie_resp_header_empty(
-# req: Request) -> Response:
-# logging.info('Python HTTP trigger function processed a request.')
-# resp = Response(
-# "This HTTP triggered function executed successfully.")
-#
-# resp.headers.add("Set-Cookie", '')
-#
-# return resp
diff --git a/tests/unittests/test_http_functions.py b/tests/unittests/test_http_functions.py
index 344ffdce8..50d7d6c65 100644
--- a/tests/unittests/test_http_functions.py
+++ b/tests/unittests/test_http_functions.py
@@ -462,8 +462,3 @@ def test_no_return_returns(self):
r = self.webhost.request('GET', 'no_return_returns')
self.assertEqual(r.status_code, 200)
-class TestHttpFunctionsV2(TestHttpFunctions):
- @classmethod
- def get_script_dir(cls):
- return testutils.UNIT_TESTS_FOLDER / 'http_functions' / \
- 'http_v2_functions'
diff --git a/tests/unittests/test_http_functions_v2.py b/tests/unittests/test_http_functions_v2.py
new file mode 100644
index 000000000..7c5adcd5d
--- /dev/null
+++ b/tests/unittests/test_http_functions_v2.py
@@ -0,0 +1,457 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+import filecmp
+import hashlib
+import os
+import pathlib
+import sys
+import typing
+from unittest import skipIf
+from unittest.mock import patch
+
+from azure_functions_worker.constants import PYTHON_ENABLE_INIT_INDEXING
+from tests.utils import testutils
+
+
+class TestHttpFunctionsV2FastApi(testutils.WebHostTestCase):
+ @classmethod
+ def setUpClass(cls):
+ os_environ = os.environ.copy()
+ # Turn on feature flag
+ os_environ[PYTHON_ENABLE_INIT_INDEXING] = '1'
+ cls._patch_environ = patch.dict('os.environ', os_environ)
+ cls._patch_environ.start()
+
+ super().setUpClass()
+
+ @classmethod
+ def tearDownClass(cls):
+ cls._patch_environ.stop()
+ super().tearDownClass()
+
+ @classmethod
+ def get_script_dir(cls):
+ return testutils.UNIT_TESTS_FOLDER / 'http_functions' / \
+ 'http_v2_functions' / \
+ 'fastapi'
+
+ def test_return_bytes(self):
+ r = self.webhost.request('GET', 'return_bytes')
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(r.content, b'"Hello World"')
+ self.assertEqual(r.headers['content-type'], 'application/json')
+
+ def test_return_http_200(self):
+ r = self.webhost.request('GET', 'return_http')
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(r.text, 'Hello World™
')
+ self.assertEqual(r.headers['content-type'], 'text/html; charset=utf-8')
+
+ def test_return_http_no_body(self):
+ r = self.webhost.request('GET', 'return_http_no_body')
+ self.assertEqual(r.text, '')
+ self.assertEqual(r.status_code, 200)
+
+ def test_return_http_auth_level_admin(self):
+ r = self.webhost.request('GET', 'return_http_auth_admin',
+ params={'code': 'testMasterKey'})
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(r.text, 'Hello World™
')
+ self.assertEqual(r.headers['content-type'], 'text/html; charset=utf-8')
+
+ def test_return_http_404(self):
+ r = self.webhost.request('GET', 'return_http_404')
+ self.assertEqual(r.status_code, 404)
+ self.assertEqual(r.text, 'bye')
+
+ def test_return_http_redirect(self):
+ r = self.webhost.request('GET', 'return_http_redirect')
+ self.assertEqual(r.text, 'Hello World™
')
+ self.assertEqual(r.status_code, 200)
+
+ r = self.webhost.request('GET', 'return_http_redirect',
+ allow_redirects=False)
+ self.assertEqual(r.status_code, 302)
+
+ def test_async_return_str(self):
+ r = self.webhost.request('GET', 'async_return_str')
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(r.text, '"Hello Async World!"')
+
+ def test_async_logging(self):
+ # Test that logging doesn't *break* things.
+ r = self.webhost.request('GET', 'async_logging')
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(r.text, '"OK-async"')
+
+ def check_log_async_logging(self, host_out: typing.List[str]):
+ # Host out only contains user logs
+ self.assertIn('hello info', host_out)
+ self.assertIn('and another error', host_out)
+
+ def test_debug_logging(self):
+ r = self.webhost.request('GET', 'debug_logging')
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(r.text, '"OK-debug"')
+
+ def check_log_debug_logging(self, host_out: typing.List[str]):
+ self.assertIn('logging info', host_out)
+ self.assertIn('logging warning', host_out)
+ self.assertIn('logging error', host_out)
+ self.assertNotIn('logging debug', host_out)
+
+ def test_debug_with_user_logging(self):
+ r = self.webhost.request('GET', 'debug_user_logging')
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(r.text, '"OK-user-debug"')
+
+ def check_log_debug_with_user_logging(self, host_out: typing.List[str]):
+ self.assertIn('logging info', host_out)
+ self.assertIn('logging warning', host_out)
+ self.assertIn('logging debug', host_out)
+ self.assertIn('logging error', host_out)
+
+ def test_sync_logging(self):
+ # Test that logging doesn't *break* things.
+ r = self.webhost.request('GET', 'sync_logging')
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(r.text, '"OK-sync"')
+
+ def check_log_sync_logging(self, host_out: typing.List[str]):
+ # Host out only contains user logs
+ self.assertIn('a gracefully handled error', host_out)
+
+ def test_return_context(self):
+ r = self.webhost.request('GET', 'return_context')
+ self.assertEqual(r.status_code, 200)
+
+ data = r.json()
+
+ self.assertEqual(data['method'], 'GET')
+ self.assertEqual(data['ctx_func_name'], 'return_context')
+ self.assertIn('ctx_invocation_id', data)
+ self.assertIn('ctx_trace_context_Tracestate', data)
+ self.assertIn('ctx_trace_context_Traceparent', data)
+
+ def test_remapped_context(self):
+ r = self.webhost.request('GET', 'remapped_context')
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(r.text, '"GET"')
+
+ def test_return_request(self):
+ r = self.webhost.request(
+ 'GET', 'return_request',
+ params={'a': 1, 'b': ':%)'},
+ headers={'xxx': 'zzz', 'Max-Forwards': '10'})
+
+ self.assertEqual(r.status_code, 200)
+
+ req = r.json()
+
+ self.assertEqual(req['method'], 'GET')
+ self.assertEqual(req['params'], {'a': '1', 'b': ':%)'})
+ self.assertEqual(req['headers']['xxx'], 'zzz')
+ self.assertEqual(req['headers']['max-forwards'], '10')
+
+ self.assertIn('return_request', req['url'])
+
+ def test_post_return_request(self):
+ r = self.webhost.request(
+ 'POST', 'return_request',
+ params={'a': 1, 'b': ':%)'},
+ headers={'xxx': 'zzz'},
+ data={'key': 'value'})
+
+ self.assertEqual(r.status_code, 200)
+
+ req = r.json()
+
+ self.assertEqual(req['method'], 'POST')
+ self.assertEqual(req['params'], {'a': '1', 'b': ':%)'})
+ self.assertEqual(req['headers']['xxx'], 'zzz')
+
+ self.assertIn('return_request', req['url'])
+
+ self.assertEqual(req['body'], 'key=value')
+
+ def test_post_json_request_is_untouched(self):
+ body = b'{"foo": "bar", "two": 4}'
+ body_hash = hashlib.sha256(body).hexdigest()
+ r = self.webhost.request(
+ 'POST', 'return_request',
+ headers={'Content-Type': 'application/json'},
+ data=body)
+
+ self.assertEqual(r.status_code, 200)
+ req = r.json()
+ self.assertEqual(req['body_hash'], body_hash)
+
+ def test_accept_json(self):
+ r = self.webhost.request(
+ 'GET', 'accept_json',
+ json={'a': 'abc', 'd': 42})
+
+ self.assertEqual(r.status_code, 200)
+ r_json = r.json()
+ self.assertEqual(r_json, {'a': 'abc', 'd': 42})
+ self.assertEqual(r.headers['content-type'], 'application/json')
+
+ def test_unhandled_error(self):
+ r = self.webhost.request('GET', 'unhandled_error')
+ self.assertEqual(r.status_code, 500)
+ # https://github.com/Azure/azure-functions-host/issues/2706
+ # self.assertIn('Exception: ZeroDivisionError', r.text)
+
+ def check_log_unhandled_error(self,
+ host_out: typing.List[str]):
+ self.assertIn('Exception: ZeroDivisionError: division by zero',
+ host_out)
+
+ def test_unhandled_urllib_error(self):
+ r = self.webhost.request(
+ 'GET', 'unhandled_urllib_error',
+ params={'img': 'http://example.com/nonexistent.jpg'})
+ self.assertEqual(r.status_code, 500)
+
+ def test_unhandled_unserializable_error(self):
+ r = self.webhost.request(
+ 'GET', 'unhandled_unserializable_error')
+ self.assertEqual(r.status_code, 500)
+
+ def test_return_route_params(self):
+ r = self.webhost.request('GET', 'return_route_params/foo/bar')
+ self.assertEqual(r.status_code, 200)
+ resp = r.json()
+ self.assertEqual(resp, {'param1': 'foo', 'param2': 'bar'})
+
+ def test_raw_body_bytes(self):
+ parent_dir = pathlib.Path(__file__).parent
+ image_file = parent_dir / 'resources/functions.png'
+ with open(image_file, 'rb') as image:
+ img = image.read()
+ img_len = len(img)
+ r = self.webhost.request('POST', 'raw_body_bytes', data=img)
+
+ received_body_len = int(r.headers['body-len'])
+ self.assertEqual(received_body_len, img_len)
+
+ body = r.content
+ try:
+ received_img_file = parent_dir / 'received_img.png'
+ with open(received_img_file, 'wb') as received_img:
+ received_img.write(body)
+ self.assertTrue(filecmp.cmp(received_img_file, image_file))
+ finally:
+ if (os.path.exists(received_img_file)):
+ os.remove(received_img_file)
+
+ def test_image_png_content_type(self):
+ parent_dir = pathlib.Path(__file__).parent
+ image_file = parent_dir / 'resources/functions.png'
+ with open(image_file, 'rb') as image:
+ img = image.read()
+ img_len = len(img)
+ r = self.webhost.request(
+ 'POST', 'raw_body_bytes',
+ headers={'Content-Type': 'image/png'},
+ data=img)
+
+ received_body_len = int(r.headers['body-len'])
+ self.assertEqual(received_body_len, img_len)
+
+ body = r.content
+ try:
+ received_img_file = parent_dir / 'received_img.png'
+ with open(received_img_file, 'wb') as received_img:
+ received_img.write(body)
+ self.assertTrue(filecmp.cmp(received_img_file, image_file))
+ finally:
+ if (os.path.exists(received_img_file)):
+ os.remove(received_img_file)
+
+ def test_application_octet_stream_content_type(self):
+ parent_dir = pathlib.Path(__file__).parent
+ image_file = parent_dir / 'resources/functions.png'
+ with open(image_file, 'rb') as image:
+ img = image.read()
+ img_len = len(img)
+ r = self.webhost.request(
+ 'POST', 'raw_body_bytes',
+ headers={'Content-Type': 'application/octet-stream'},
+ data=img)
+
+ received_body_len = int(r.headers['body-len'])
+ self.assertEqual(received_body_len, img_len)
+
+ body = r.content
+ try:
+ received_img_file = parent_dir / 'received_img.png'
+ with open(received_img_file, 'wb') as received_img:
+ received_img.write(body)
+ self.assertTrue(filecmp.cmp(received_img_file, image_file))
+ finally:
+ if (os.path.exists(received_img_file)):
+ os.remove(received_img_file)
+
+ def test_user_event_loop_error(self):
+ # User event loop is not supported in HTTP trigger
+ r = self.webhost.request('GET', 'user_event_loop/')
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(r.text, '"OK-user-event-loop"')
+
+ def check_log_user_event_loop_error(self, host_out: typing.List[str]):
+ self.assertIn('try_log', host_out)
+
+ def check_log_import_module_troubleshooting_url(self,
+ host_out: typing.List[str]):
+ passed = False
+ exception_message = "Exception: ModuleNotFoundError: "\
+ "No module named 'does_not_exist'. "\
+ "Cannot find module. "\
+ "Please check the requirements.txt file for the "\
+ "missing module. For more info, please refer the "\
+ "troubleshooting guide: "\
+ "https://aka.ms/functions-modulenotfound. "\
+ "Current sys.path: "
+ for log in host_out:
+ if exception_message in log:
+ passed = True
+ self.assertTrue(passed)
+
+ def test_print_logging_no_flush(self):
+ r = self.webhost.request('GET', 'print_logging?message=Secret42')
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(r.text, '"OK-print-logging"')
+
+ def check_log_print_logging_no_flush(self, host_out: typing.List[str]):
+ self.assertIn('Secret42', host_out)
+
+ def test_print_logging_with_flush(self):
+ r = self.webhost.request('GET',
+ 'print_logging?flush=true&message=Secret42')
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(r.text, '"OK-print-logging"')
+
+ def check_log_print_logging_with_flush(self, host_out: typing.List[str]):
+ self.assertIn('Secret42', host_out)
+
+ def test_print_to_console_stdout(self):
+ r = self.webhost.request('GET',
+ 'print_logging?console=true&message=Secret42')
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(r.text, '"OK-print-logging"')
+
+ @skipIf(sys.version_info < (3, 8, 0),
+ "Skip the tests for Python 3.7 and below")
+ def test_multiple_cookie_header_in_response(self):
+ r = self.webhost.request('GET', 'multiple_set_cookie_resp_headers')
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(r.headers.get(
+ 'Set-Cookie'),
+ "foo3=42; Domain=example.com; expires=Thu, 12 Jan 2017 13:55:08"
+ " GMT; HttpOnly; Max-Age=10000000; Path=/; SameSite=Lax; Secure,"
+ " foo3=43; Domain=example.com; expires=Fri, 12 Jan 2018 13:55:08"
+ " GMT; HttpOnly; Max-Age=10000000; Path=/; SameSite=Lax; Secure")
+
+ @skipIf(sys.version_info < (3, 8, 0),
+ "Skip the tests for Python 3.7 and below")
+ def test_set_cookie_header_in_response_default_value(self):
+ r = self.webhost.request('GET',
+ 'set_cookie_resp_header_default_values')
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(r.headers.get('Set-Cookie'),
+ 'foo3=42; Path=/; SameSite=lax')
+
+ @skipIf(sys.version_info < (3, 8, 0),
+ "Skip the tests for Python 3.7 and below")
+ def test_response_cookie_header_nullable_timestamp_err(self):
+ r = self.webhost.request(
+ 'GET',
+ 'response_cookie_header_nullable_timestamp_err')
+ self.assertEqual(r.status_code, 200)
+
+
+ @skipIf(sys.version_info < (3, 8, 0),
+ "Skip the tests for Python 3.7 and below")
+ def test_response_cookie_header_nullable_bool_err(self):
+ r = self.webhost.request(
+ 'GET',
+ 'response_cookie_header_nullable_bool_err')
+ self.assertEqual(r.status_code, 200)
+ self.assertTrue("Set-Cookie" in r.headers)
+
+
+ def test_print_to_console_stderr(self):
+ r = self.webhost.request('GET', 'print_logging?console=true'
+ '&message=Secret42&is_stderr=true')
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(r.text, '"OK-print-logging"')
+
+ def check_log_print_to_console_stderr(self, host_out: typing.List[str], ):
+ # System logs stderr should not exist in host_out
+ self.assertNotIn('Secret42', host_out)
+
+ def test_hijack_current_event_loop(self):
+ r = self.webhost.request('GET', 'hijack_current_event_loop/')
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(r.text, '"OK-hijack-current-event-loop"')
+
+ def check_log_hijack_current_event_loop(self, host_out: typing.List[str]):
+ # User logs should exist in host_out
+ self.assertIn('parallelly_print', host_out)
+ self.assertIn('parallelly_log_info at root logger', host_out)
+ self.assertIn('parallelly_log_warning at root logger', host_out)
+ self.assertIn('parallelly_log_error at root logger', host_out)
+ self.assertIn('parallelly_log_exception at root logger', host_out)
+ self.assertIn('parallelly_log_custom at custom_logger', host_out)
+ self.assertIn('callsoon_log', host_out)
+
+ # System logs should not exist in host_out
+ self.assertNotIn('parallelly_log_system at disguised_logger', host_out)
+
+ def test_no_type_hint(self):
+ r = self.webhost.request('GET', 'no_type_hint')
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(r.text, '"no_type_hint"')
+
+ def test_return_int(self):
+ r = self.webhost.request('GET', 'return_int')
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(r.text, '1000')
+
+ def test_return_float(self):
+ r = self.webhost.request('GET', 'return_float')
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(r.text, '1000.0')
+
+ def test_return_bool(self):
+ r = self.webhost.request('GET', 'return_bool')
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(r.text, 'true')
+
+ def test_return_dict(self):
+ r = self.webhost.request('GET', 'return_dict')
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(r.json(), {'key': 'value'})
+
+ def test_return_list(self):
+ r = self.webhost.request('GET', 'return_list')
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(r.json(), ["value1", "value2"])
+
+ def test_return_pydantic_model(self):
+ r = self.webhost.request('GET', 'return_pydantic_model')
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(r.json(), {'description': 'description1',
+ 'name': 'item1'})
+
+ def test_return_pydantic_model_with_missing_fields(self):
+ r = self.webhost.request('GET',
+ 'return_pydantic_model_with_missing_fields')
+ self.assertEqual(r.status_code, 500)
+
+ def check_return_pydantic_model_with_missing_fields(self,
+ host_out:
+ typing.List[str]):
+ self.assertIn("Field required [type=missing, input_value={'name': "
+ "'item1'}, input_type=dict]", host_out)
\ No newline at end of file
From dffc81f6125c9fcec51793a3ac2475366c44f5f1 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Thu, 4 Apr 2024 22:59:30 -0700
Subject: [PATCH 003/101] fix ppl
---
.github/workflows/ci_e2e_workflow.yml | 1 +
.github/workflows/ci_ut_workflow.yml | 3 ++-
setup.py | 2 +-
3 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/ci_e2e_workflow.yml b/.github/workflows/ci_e2e_workflow.yml
index 38c04946d..b92822ab5 100644
--- a/.github/workflows/ci_e2e_workflow.yml
+++ b/.github/workflows/ci_e2e_workflow.yml
@@ -65,6 +65,7 @@ jobs:
python -m pip install --upgrade pip
python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre
python -m pip install -U -e .[dev]
+ python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2]
# Retry a couple times to avoid certificate issue
retry 5 python setup.py build
diff --git a/.github/workflows/ci_ut_workflow.yml b/.github/workflows/ci_ut_workflow.yml
index 557f6a0bb..9cbe59fc1 100644
--- a/.github/workflows/ci_ut_workflow.yml
+++ b/.github/workflows/ci_ut_workflow.yml
@@ -59,7 +59,8 @@ jobs:
python -m pip install --upgrade pip
python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre
python -m pip install -U -e .[dev]
-
+ python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-deferred-bindings]
+
# Retry a couple times to avoid certificate issue
retry 5 python setup.py build
retry 5 python setup.py webhost --branch-name=dev
diff --git a/setup.py b/setup.py
index a4f3095a0..5308ae768 100644
--- a/setup.py
+++ b/setup.py
@@ -110,7 +110,7 @@
"numpy",
"pre-commit"
],
- "http-v2": ["azure-functions-extension-fastapi", "ujson", "orjson"]
+ "test-http-v2": ["azure-functions-extension-fastapi", "ujson", "orjson"]
}
From fa52c76db7b93b8fc46e06474555962f6376e0e0 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Thu, 4 Apr 2024 23:29:50 -0700
Subject: [PATCH 004/101] revert
---
python/test/worker.config.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/python/test/worker.config.json b/python/test/worker.config.json
index d530908fc..3fc2a9236 100644
--- a/python/test/worker.config.json
+++ b/python/test/worker.config.json
@@ -2,7 +2,7 @@
"description":{
"language":"python",
"extensions":[".py"],
- "defaultExecutablePath":"E:\\projects\\AzureFunctionsPythonWorker\\.venv_3.9_debug\\Scripts\\python.exe",
+ "defaultExecutablePath":"python",
"defaultWorkerPath":"worker.py",
"workerIndexing": "true"
}
From de028b6353cedbb6060ce41ca96fe8cf6aed3223 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Thu, 4 Apr 2024 23:34:44 -0700
Subject: [PATCH 005/101] fix
---
.github/workflows/ci_consumption_workflow.yml | 1 +
.github/workflows/ci_ut_workflow.yml | 4 ++--
2 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/ci_consumption_workflow.yml b/.github/workflows/ci_consumption_workflow.yml
index 907e3de4c..e62d6b1b6 100644
--- a/.github/workflows/ci_consumption_workflow.yml
+++ b/.github/workflows/ci_consumption_workflow.yml
@@ -34,6 +34,7 @@ jobs:
run: |
python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre
python -m pip install -U -e .[dev]
+ python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2]
python setup.py build
- name: Running 3.7 Tests
if: matrix.python-version == 3.7
diff --git a/.github/workflows/ci_ut_workflow.yml b/.github/workflows/ci_ut_workflow.yml
index 53a62dfa8..1e3547955 100644
--- a/.github/workflows/ci_ut_workflow.yml
+++ b/.github/workflows/ci_ut_workflow.yml
@@ -59,8 +59,8 @@ jobs:
python -m pip install --upgrade pip
python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre
python -m pip install -U -e .[dev]
- python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-deferred-bindings]
-
+ python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2]
+
# Retry a couple times to avoid certificate issue
retry 5 python setup.py build
retry 5 python setup.py webhost --branch-name=dev
From d5987cf49f5709de45cd03393aa0a1b76671f233 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Fri, 5 Apr 2024 01:19:52 -0700
Subject: [PATCH 006/101] fix
---
azure_functions_worker/dispatcher.py | 2 +-
azure_functions_worker/{http_proxy.py => http_v2.py} | 0
azure_functions_worker/logging.py | 2 --
3 files changed, 1 insertion(+), 3 deletions(-)
rename azure_functions_worker/{http_proxy.py => http_v2.py} (100%)
diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py
index 641b2a25e..b13c5511d 100644
--- a/azure_functions_worker/dispatcher.py
+++ b/azure_functions_worker/dispatcher.py
@@ -35,7 +35,7 @@
X_MS_INVOCATION_ID, LOCAL_HOST,
METADATA_PROPERTIES_WORKER_INDEXED)
from .extension import ExtensionManager
-from .http_proxy import http_coordinator
+from .http_v2 import http_coordinator
from .logging import disable_console_logging, enable_console_logging
from .logging import (logger, error_logger, is_system_log_category,
CONSOLE_LOG_PREFIX, format_exception)
diff --git a/azure_functions_worker/http_proxy.py b/azure_functions_worker/http_v2.py
similarity index 100%
rename from azure_functions_worker/http_proxy.py
rename to azure_functions_worker/http_v2.py
diff --git a/azure_functions_worker/logging.py b/azure_functions_worker/logging.py
index ddc5a7faf..9f733c651 100644
--- a/azure_functions_worker/logging.py
+++ b/azure_functions_worker/logging.py
@@ -20,8 +20,6 @@
handler: Optional[logging.Handler] = None
error_handler: Optional[logging.Handler] = None
-# local_handler = logging.FileHandler("E:/projects/AzureFunctionsPythonWorker/log.txt")
-# logger.addHandler(local_handler)
def format_exception(exception: Exception) -> str:
msg = str(exception) + "\n"
From 2410771c9dbb1afde33f6324298d8b4b91940e21 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Fri, 5 Apr 2024 11:00:12 -0700
Subject: [PATCH 007/101] fix
---
azure_functions_worker/functions.py | 2 +-
azure_functions_worker/http_v2.py | 33 +++++---
tests/unittests/test_http_v2.py | 127 ++++++++++++++++++++++++++++
3 files changed, 149 insertions(+), 13 deletions(-)
create mode 100644 tests/unittests/test_http_v2.py
diff --git a/azure_functions_worker/functions.py b/azure_functions_worker/functions.py
index 0200c01ae..52c79847c 100644
--- a/azure_functions_worker/functions.py
+++ b/azure_functions_worker/functions.py
@@ -33,7 +33,7 @@ class FunctionInfo(typing.NamedTuple):
output_types: typing.Mapping[str, ParamTypeInfo]
return_type: typing.Optional[ParamTypeInfo]
- trigger_metadata: typing.Dict[str, typing.Any]
+ trigger_metadata: typing.Optional[Dict[str, typing.Any]]
class FunctionLoadError(RuntimeError):
diff --git a/azure_functions_worker/http_v2.py b/azure_functions_worker/http_v2.py
index b56213485..8209ea80c 100644
--- a/azure_functions_worker/http_v2.py
+++ b/azure_functions_worker/http_v2.py
@@ -2,8 +2,11 @@
import asyncio
from typing import Dict
+
class BaseContextReference(abc.ABC):
- def __init__(self, event_class, http_request=None, http_response=None, function=None, fi_context=None, args=None, http_trigger_param_name=None):
+ def __init__(self, event_class, http_request=None, http_response=None,
+ function=None, fi_context=None, args=None,
+ http_trigger_param_name=None):
self._http_request = http_request
self._http_response = http_response
self._function = function
@@ -78,8 +81,10 @@ def rpc_invocation_ready_event(self):
class AsyncContextReference(BaseContextReference):
- def __init__(self, http_request=None, http_response=None, function=None, fi_context=None, args=None):
- super().__init__(event_class=asyncio.Event, http_request=http_request, http_response=http_response,
+ def __init__(self, http_request=None, http_response=None, function=None,
+ fi_context=None, args=None):
+ super().__init__(event_class=asyncio.Event, http_request=http_request,
+ http_response=http_response,
function=function, fi_context=fi_context, args=args)
self.is_async = True
@@ -96,7 +101,7 @@ def __call__(cls, *args, **kwargs):
class HttpCoordinator(metaclass=SingletonMeta):
def __init__(self):
self._context_references: Dict[str, BaseContextReference] = {}
-
+
def set_http_request(self, invoc_id, http_request):
if invoc_id not in self._context_references:
self._context_references[invoc_id] = AsyncContextReference()
@@ -105,32 +110,36 @@ def set_http_request(self, invoc_id, http_request):
def set_http_response(self, invoc_id, http_response):
if invoc_id not in self._context_references:
- raise Exception("No context reference found for invocation %s", invoc_id)
+ raise Exception("No context reference found for invocation %s",
+ invoc_id)
context_ref = self._context_references.get(invoc_id)
context_ref.http_response = http_response
async def get_http_request_async(self, invoc_id):
if invoc_id not in self._context_references:
self._context_references[invoc_id] = AsyncContextReference()
-
+
await asyncio.sleep(0)
- await self._context_references.get(invoc_id).http_request_available_event.wait()
+ await self._context_references.get(
+ invoc_id).http_request_available_event.wait()
return self._pop_http_request(invoc_id)
async def await_http_response_async(self, invoc_id):
if invoc_id not in self._context_references:
- raise Exception("No context reference found for invocation %s", invoc_id)
+ raise Exception("No context reference found for invocation %s",
+ invoc_id)
await asyncio.sleep(0)
- await self._context_references.get(invoc_id).http_response_available_event.wait()
+ await self._context_references.get(
+ invoc_id).http_response_available_event.wait()
return self._pop_http_response(invoc_id)
-
+
def _pop_http_request(self, invoc_id):
context_ref = self._context_references.get(invoc_id)
request = context_ref.http_request
if request is not None:
context_ref.http_request = None
return request
-
+
raise Exception("No http request found for invocation %s", invoc_id)
def _pop_http_response(self, invoc_id):
@@ -139,7 +148,7 @@ def _pop_http_response(self, invoc_id):
if response is not None:
context_ref.http_response = None
return response
- # If user does not set the response, return nothing and web server will return 200 empty response
+ raise Exception("No http response found for invocation %s", invoc_id)
http_coordinator = HttpCoordinator()
diff --git a/tests/unittests/test_http_v2.py b/tests/unittests/test_http_v2.py
new file mode 100644
index 000000000..64aefe56f
--- /dev/null
+++ b/tests/unittests/test_http_v2.py
@@ -0,0 +1,127 @@
+import asyncio
+import unittest
+from unittest.mock import MagicMock
+
+from azure_functions_worker.http_v2 import http_coordinator
+
+
+class MockHttpRequest:
+ pass
+
+
+class MockHttpResponse:
+ pass
+
+
+class TestHttpCoordinator(unittest.TestCase):
+ def setUp(self):
+ self.invoc_id = "test_invocation"
+ self.http_request = MockHttpRequest()
+ self.http_response = MockHttpResponse()
+
+ def tearDown(self) -> None:
+ http_coordinator._context_references.clear()
+
+ def test_set_http_request_new_invocation(self):
+ # Test setting a new HTTP request
+ http_coordinator.set_http_request(self.invoc_id, self.http_request)
+ context_ref = http_coordinator._context_references.get(self.invoc_id)
+ self.assertIsNotNone(context_ref)
+ self.assertEqual(context_ref.http_request, self.http_request)
+
+ def test_set_http_request_existing_invocation(self):
+ # Test updating an existing HTTP request
+ new_http_request = MagicMock()
+ http_coordinator.set_http_request(self.invoc_id, self.http_request)
+ http_coordinator.set_http_request(self.invoc_id, new_http_request)
+ context_ref = http_coordinator._context_references.get(self.invoc_id)
+ self.assertIsNotNone(context_ref)
+ self.assertEqual(context_ref.http_request, new_http_request)
+
+ def test_set_http_response_context_ref_null(self):
+ with self.assertRaises(Exception) as cm:
+ http_coordinator.set_http_response(self.invoc_id,
+ self.http_response)
+ self.assertEqual(cm.exception.args[0],
+ "No context reference found for invocation %s")
+
+ def test_set_http_response(self):
+ http_coordinator.set_http_request(self.invoc_id, self.http_request)
+ http_coordinator.set_http_response(self.invoc_id, self.http_response)
+ context_ref = http_coordinator._context_references[self.invoc_id]
+ self.assertEqual(context_ref.http_response, self.http_response)
+
+ def test_get_http_request_async_existing_invocation(self):
+ # Test retrieving an existing HTTP request
+ http_coordinator.set_http_request(self.invoc_id,
+ self.http_request)
+ loop = asyncio.get_event_loop()
+ retrieved_request = loop.run_until_complete(
+ http_coordinator.get_http_request_async(self.invoc_id))
+ self.assertEqual(retrieved_request, self.http_request)
+
+ def test_get_http_request_async_wait_for_request(self):
+ # Test waiting for an HTTP request to become available
+ async def set_request_after_delay():
+ await asyncio.sleep(1)
+ http_coordinator.set_http_request(self.invoc_id,
+ self.http_request)
+
+ loop = asyncio.get_event_loop()
+ loop.create_task(set_request_after_delay())
+ retrieved_request = loop.run_until_complete(
+ http_coordinator.get_http_request_async(self.invoc_id))
+ self.assertEqual(retrieved_request, self.http_request)
+
+ def test_get_http_request_async_wait_forever(self):
+ # Test handling error when invoc_id is not found
+ invalid_invoc_id = "invalid_invocation"
+ loop = asyncio.get_event_loop()
+ with self.assertRaises(asyncio.TimeoutError):
+ loop.run_until_complete(
+ asyncio.wait_for(
+ http_coordinator.get_http_request_async(invalid_invoc_id),
+ timeout=1
+ )
+ )
+
+ async def test_await_http_response_async_valid_invocation(self):
+ invoc_id = "valid_invocation"
+ expected_response = self.http_response
+
+ context_ref = {}
+ context_ref.http_response = expected_response
+
+ # Add the mock context reference to the coordinator
+ http_coordinator._context_references[invoc_id] = context_ref
+
+ # Call the method and verify the returned response
+ response = await http_coordinator.await_http_response_async(invoc_id)
+ self.assertEqual(response, expected_response)
+ self.assertTrue(
+ http_coordinator._context_references.get(
+ invoc_id).http_response is None)
+
+ async def test_await_http_response_async_invalid_invocation(self):
+ # Test handling error when invoc_id is not found
+ invalid_invoc_id = "invalid_invocation"
+ with self.assertRaises(Exception) as context:
+ await http_coordinator.await_http_response_async(invalid_invoc_id)
+ self.assertEqual(str(context.exception),
+ f"No context reference found for invocation "
+ f"{invalid_invoc_id}")
+
+ async def test_await_http_response_async_response_not_set(self):
+ invoc_id = "invocation_with_no_response"
+ # Set up a mock context reference without setting the response
+ context_ref = {}
+ context_ref.http_response = None
+
+ # Add the mock context reference to the coordinator
+ http_coordinator._context_references[invoc_id] = context_ref
+
+ # Call the method and verify that it raises an exception
+ with self.assertRaises(Exception) as context:
+ await http_coordinator.await_http_response_async(invoc_id)
+ self.assertEqual(str(context.exception),
+ f"No http response found for invocation {invoc_id}")
From b78e549e6aa5ff11f476a1a0dba0b2eaa8fa8c51 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Fri, 5 Apr 2024 11:26:47 -0700
Subject: [PATCH 008/101] fix
---
azure_functions_worker/functions.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/azure_functions_worker/functions.py b/azure_functions_worker/functions.py
index 52c79847c..1e94a39c0 100644
--- a/azure_functions_worker/functions.py
+++ b/azure_functions_worker/functions.py
@@ -33,7 +33,7 @@ class FunctionInfo(typing.NamedTuple):
output_types: typing.Mapping[str, ParamTypeInfo]
return_type: typing.Optional[ParamTypeInfo]
- trigger_metadata: typing.Optional[Dict[str, typing.Any]]
+ trigger_metadata: typing.Optional[typing.Dict[str, typing.Any]]
class FunctionLoadError(RuntimeError):
From 58e89f08a292fbeae9d94559390bd6295bb25594 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Fri, 5 Apr 2024 11:46:57 -0700
Subject: [PATCH 009/101] flake8
---
azure_functions_worker/dispatcher.py | 65 +++++++++++++++-------------
1 file changed, 34 insertions(+), 31 deletions(-)
diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py
index b13c5511d..bc3787267 100644
--- a/azure_functions_worker/dispatcher.py
+++ b/azure_functions_worker/dispatcher.py
@@ -146,7 +146,7 @@ async def connect(cls, host: str, port: int, worker_id: str,
async def dispatch_forever(self): # sourcery skip: swap-if-expression
if DispatcherMeta.__current_dispatcher__ is not None:
raise RuntimeError('there can be only one running dispatcher per '
- 'process')
+ 'process')
self._old_task_factory = self._loop.get_task_factory()
@@ -174,10 +174,9 @@ async def dispatch_forever(self): # sourcery skip: swap-if-expression
logging_handler = AsyncLoggingHandler()
root_logger = logging.getLogger()
-
log_level = logging.INFO if not is_envvar_true(
PYTHON_ENABLE_DEBUG_LOGGING) else logging.DEBUG
-
+
root_logger.setLevel(log_level)
root_logger.addHandler(logging_handler)
logger.info('Switched to gRPC logging.')
@@ -285,20 +284,19 @@ async def _dispatch_grpc_request(self, request):
async def _handle__worker_init_request(self, request):
try:
logger.info('Received WorkerInitRequest, '
- 'python version %s, '
- 'worker version %s, '
- 'request ID %s. '
- 'App Settings state: %s. '
- 'To enable debug level logging, please refer to '
- 'https://aka.ms/python-enable-debug-logging',
- sys.version,
- VERSION,
- self.request_id,
- get_python_appsetting_state()
- )
+ 'python version %s, '
+ 'worker version %s, '
+ 'request ID %s. '
+ 'App Settings state: %s. '
+ 'To enable debug level logging, please refer to '
+ 'https://aka.ms/python-enable-debug-logging',
+ sys.version,
+ VERSION,
+ self.request_id,
+ get_python_appsetting_state()
+ )
worker_init_request = request.worker_init_request
- directory = worker_init_request.function_app_directory
host_capabilities = worker_init_request.capabilities
if constants.FUNCTION_DATA_CACHE in host_capabilities:
val = host_capabilities[constants.FUNCTION_DATA_CACHE]
@@ -332,10 +330,11 @@ async def _handle__worker_init_request(self, request):
self._function_metadata_exception = ex
if self._has_http_func:
- from azure.functions.extension.base import HttpV2FeatureChecker
+ from azure.functions.extension.base import HttpV2FeatureChecker
if HttpV2FeatureChecker.http_v2_enabled():
- capabilities[constants.HTTP_URI] = await self._initialize_http_server()
+ capabilities[constants.HTTP_URI] = \
+ await self._initialize_http_server()
return protos.StreamingMessage(
request_id=self.request_id,
@@ -386,7 +385,6 @@ def load_function_metadata(self, function_app_directory, caller_info):
self.index_functions(function_path)) \
if os.path.exists(function_path) else None
-
async def _handle__functions_metadata_request(self, request):
metadata_request = request.functions_metadata_request
function_app_directory = metadata_request.function_app_directory
@@ -555,7 +553,7 @@ async def _handle__invocation_request(self, request):
trigger_metadata = None
if bindings.is_trigger_binding(pb_type_info.binding_name):
trigger_metadata = invoc_request.trigger_metadata
-
+
args[pb.name] = bindings.from_incoming_proto(
pb_type_info.binding_name, pb,
trigger_metadata=trigger_metadata,
@@ -570,11 +568,14 @@ async def _handle__invocation_request(self, request):
if http_v2_enabled:
http_request = await http_coordinator.get_http_request_async(
invocation_id)
-
+
from azure.functions.extension.base import RequestTrackerMeta
- route_params = {key: item.string for key, item in trigger_metadata.items() if key not in ['Headers', 'Query']}
+ route_params = {key: item.string for key, item
+ in trigger_metadata.items() if key not in [
+ 'Headers', 'Query']}
- RequestTrackerMeta.get_synchronizer().sync_route_params(http_request, route_params)
+ (RequestTrackerMeta.get_synchronizer()
+ .sync_route_params(http_request, route_params))
args[fi.trigger_metadata.get('param_name')] = http_request
fi_context = self._get_context(invoc_request, fi.name, fi.directory)
@@ -599,16 +600,18 @@ async def _handle__invocation_request(self, request):
self._sync_call_tp,
self._run_sync_func,
invocation_id, fi_context, fi.func, args)
-
+
if call_result is not None and not fi.has_return:
raise RuntimeError(f'function {fi.name!r} without a $return '
- 'binding returned a non-None value')
+ 'binding returned a non-None value')
except Exception as e:
call_error = e
raise
finally:
if http_v2_enabled:
- http_coordinator.set_http_response(invocation_id, call_result if call_result is not None else call_error)
+ http_coordinator.set_http_response(
+ invocation_id, call_result
+ if call_result is not None else call_error)
output_data = []
cache_enabled = self._function_data_cache_enabled
@@ -656,7 +659,6 @@ async def _handle__invocation_request(self, request):
status=protos.StatusResult.Failure,
exception=self._serialize_exception(ex))))
-
async def _handle__function_environment_reload_request(self, request):
"""Only runs on Linux Consumption placeholder specialization.
This is called only when placeholder mode is true. On worker restarts
@@ -713,12 +715,13 @@ async def _handle__function_environment_reload_request(self, request):
caller_info="environment_reload_request")
except Exception as ex:
self._function_metadata_exception = ex
-
+
if self._has_http_func:
from azure.functions.extension.base import HttpV2FeatureChecker
if HttpV2FeatureChecker.http_v2_enabled():
- capabilities[constants.HTTP_URI] = await self._initialize_http_server()
+ capabilities[constants.HTTP_URI] = \
+ await self._initialize_http_server()
# Change function app directory
if getattr(func_env_reload_request,
@@ -748,7 +751,7 @@ async def _handle__function_environment_reload_request(self, request):
async def _initialize_http_server(self):
from azure.functions.extension.base import ModuleTrackerMeta, RequestTrackerMeta
-
+
web_extension_mod_name = ModuleTrackerMeta.get_module()
extension_module = importlib.import_module(web_extension_mod_name)
web_app_class = extension_module.WebApp
@@ -760,7 +763,7 @@ async def _initialize_http_server(self):
request_type = RequestTrackerMeta.get_request_type()
@app.route
- async def catch_all(request: request_type): # type: ignore
+ async def catch_all(request: request_type): # type: ignore
invoc_id = request.headers.get(X_MS_INVOCATION_ID)
if invoc_id is None:
raise ValueError(f"Header {X_MS_INVOCATION_ID} not found")
@@ -772,7 +775,7 @@ async def catch_all(request: request_type): # type: ignore
# if http_resp is an python exception, raise it
if isinstance(http_resp, Exception):
raise http_resp
-
+
return http_resp
web_server = web_server_class(LOCAL_HOST, unused_port, app)
From 0883bb8f2dc02038b5db11576fdab42555964f24 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Fri, 5 Apr 2024 11:56:56 -0700
Subject: [PATCH 010/101] flake8
---
azure_functions_worker/dispatcher.py | 31 ++++++++++++++++++----------
1 file changed, 20 insertions(+), 11 deletions(-)
diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py
index bc3787267..a2078f128 100644
--- a/azure_functions_worker/dispatcher.py
+++ b/azure_functions_worker/dispatcher.py
@@ -330,7 +330,8 @@ async def _handle__worker_init_request(self, request):
self._function_metadata_exception = ex
if self._has_http_func:
- from azure.functions.extension.base import HttpV2FeatureChecker
+ from azure.functions.extension.base \
+ import HttpV2FeatureChecker
if HttpV2FeatureChecker.http_v2_enabled():
capabilities[constants.HTTP_URI] = \
@@ -341,7 +342,8 @@ async def _handle__worker_init_request(self, request):
worker_init_response=protos.WorkerInitResponse(
capabilities=capabilities,
worker_metadata=self.get_worker_metadata(),
- result=protos.StatusResult(status=protos.StatusResult.Success),
+ result=protos.StatusResult(
+ status=protos.StatusResult.Success),
),
)
except Exception as e:
@@ -349,8 +351,9 @@ async def _handle__worker_init_request(self, request):
return protos.StreamingMessage(
request_id=self.request_id,
worker_init_response=protos.WorkerInitResponse(
- result=protos.StatusResult(status=protos.StatusResult.Failure,
- exception=self._serialize_exception(e))
+ result=protos.StatusResult(
+ status=protos.StatusResult.Failure,
+ exception=self._serialize_exception(e))
),
)
@@ -594,7 +597,8 @@ async def _handle__invocation_request(self, request):
call_error = None
try:
if fi.is_async:
- call_result = await self._run_async_func(fi_context, fi.func, args)
+ call_result = \
+ await self._run_async_func(fi_context, fi.func, args)
else:
call_result = await self._loop.run_in_executor(
self._sync_call_tp,
@@ -602,8 +606,9 @@ async def _handle__invocation_request(self, request):
invocation_id, fi_context, fi.func, args)
if call_result is not None and not fi.has_return:
- raise RuntimeError(f'function {fi.name!r} without a $return '
- 'binding returned a non-None value')
+ raise RuntimeError(
+ f'function {fi.name!r} without a $return binding'
+ 'returned a non-None value')
except Exception as e:
call_error = e
raise
@@ -717,7 +722,8 @@ async def _handle__function_environment_reload_request(self, request):
self._function_metadata_exception = ex
if self._has_http_func:
- from azure.functions.extension.base import HttpV2FeatureChecker
+ from azure.functions.extension.base \
+ import HttpV2FeatureChecker
if HttpV2FeatureChecker.http_v2_enabled():
capabilities[constants.HTTP_URI] = \
@@ -750,7 +756,8 @@ async def _handle__function_environment_reload_request(self, request):
function_environment_reload_response=failure_response)
async def _initialize_http_server(self):
- from azure.functions.extension.base import ModuleTrackerMeta, RequestTrackerMeta
+ from azure.functions.extension.base \
+ import ModuleTrackerMeta, RequestTrackerMeta
web_extension_mod_name = ModuleTrackerMeta.get_module()
extension_module = importlib.import_module(web_extension_mod_name)
@@ -769,7 +776,8 @@ async def catch_all(request: request_type): # type: ignore
raise ValueError(f"Header {X_MS_INVOCATION_ID} not found")
logger.info('Received HTTP request for invocation %s', invoc_id)
http_coordinator.set_http_request(invoc_id, request)
- http_resp = await http_coordinator.await_http_response_async(invoc_id)
+ http_resp = \
+ await http_coordinator.await_http_response_async(invoc_id)
logger.info('Sending HTTP response for invocation %s', invoc_id)
# if http_resp is an python exception, raise it
@@ -801,7 +809,8 @@ def index_functions(self, function_path: str):
indexed_function_logs: List[str] = []
for func in indexed_functions:
- self._has_http_func = self._has_http_func or func.is_http_function()
+ self._has_http_func = self._has_http_func or \
+ func.is_http_function()
function_log = "Function Name: {}, Function Binding: {}" \
.format(func.get_function_name(),
[(binding.type, binding.name) for binding in
From 54bd579519523f8becb2fa37e7045a10cede5ee9 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Fri, 5 Apr 2024 12:12:05 -0700
Subject: [PATCH 011/101] flake8
---
azure_functions_worker/bindings/meta.py | 12 ++++++++----
azure_functions_worker/functions.py | 5 +++--
azure_functions_worker/logging.py | 1 +
3 files changed, 12 insertions(+), 6 deletions(-)
diff --git a/azure_functions_worker/bindings/meta.py b/azure_functions_worker/bindings/meta.py
index ccfaa8200..60d22a7c2 100644
--- a/azure_functions_worker/bindings/meta.py
+++ b/azure_functions_worker/bindings/meta.py
@@ -15,19 +15,21 @@
PB_TYPE_RPC_SHARED_MEMORY = 'rpc_shared_memory'
BINDING_REGISTRY = None
+
def _check_http_input_type_annotation(bind_name: str, pytype: type) -> bool:
ext_base = sys.modules.get('azure.functions.extension.base')
if ext_base is not None and ext_base.HttpV2FeatureChecker.http_v2_enabled():
return ext_base.RequestTrackerMeta.check_type(pytype)
-
+
binding = get_binding(bind_name)
return binding.check_input_type_annotation(pytype)
+
def _check_http_output_type_annotation(bind_name: str, pytype: type) -> bool:
ext_base = sys.modules.get('azure.functions.extension.base')
if ext_base is not None and ext_base.HttpV2FeatureChecker.http_v2_enabled():
return ext_base.ResponseTrackerMeta.check_type(pytype)
-
+
binding = get_binding(bind_name)
return binding.check_output_type_annotation(pytype)
@@ -40,6 +42,7 @@ def _check_http_output_type_annotation(bind_name: str, pytype: type) -> bool:
HTTP: _check_http_output_type_annotation
}
+
def load_binding_registry() -> None:
func = sys.modules.get('azure.functions')
@@ -71,15 +74,16 @@ def check_input_type_annotation(bind_name: str, pytype: type) -> bool:
global INPUT_TYPE_CHECK_OVERRIDE_MAP
if bind_name in INPUT_TYPE_CHECK_OVERRIDE_MAP:
return INPUT_TYPE_CHECK_OVERRIDE_MAP[bind_name](bind_name, pytype)
-
+
binding = get_binding(bind_name)
return binding.check_input_type_annotation(pytype)
+
def check_output_type_annotation(bind_name: str, pytype: type) -> bool:
global OUTPUT_TYPE_CHECK_OVERRIDE_MAP
if bind_name in OUTPUT_TYPE_CHECK_OVERRIDE_MAP:
return OUTPUT_TYPE_CHECK_OVERRIDE_MAP[bind_name](bind_name, pytype)
-
+
binding = get_binding(bind_name)
return binding.check_output_type_annotation(pytype)
diff --git a/azure_functions_worker/functions.py b/azure_functions_worker/functions.py
index 1e94a39c0..ba2b8509e 100644
--- a/azure_functions_worker/functions.py
+++ b/azure_functions_worker/functions.py
@@ -35,6 +35,7 @@ class FunctionInfo(typing.NamedTuple):
trigger_metadata: typing.Optional[typing.Dict[str, typing.Any]]
+
class FunctionLoadError(RuntimeError):
def __init__(self, function_name: str, msg: str) -> None:
@@ -301,7 +302,8 @@ def add_func_to_registry_and_return_funcinfo(self, function,
return_type: str):
http_trigger_param_name = next(
- (input_type for input_type, type_info in input_types.items() if type_info.binding_name == HTTP_TRIGGER),
+ (input_type for input_type, type_info in input_types.items()
+ if type_info.binding_name == HTTP_TRIGGER),
None
)
@@ -323,7 +325,6 @@ def add_func_to_registry_and_return_funcinfo(self, function,
output_types=output_types,
return_type=return_type,
trigger_metadata=trigger_metadata)
-
self._functions[function_id] = function_info
return function_info
diff --git a/azure_functions_worker/logging.py b/azure_functions_worker/logging.py
index 9f733c651..adb5ff294 100644
--- a/azure_functions_worker/logging.py
+++ b/azure_functions_worker/logging.py
@@ -21,6 +21,7 @@
handler: Optional[logging.Handler] = None
error_handler: Optional[logging.Handler] = None
+
def format_exception(exception: Exception) -> str:
msg = str(exception) + "\n"
if (sys.version_info.major, sys.version_info.minor) < (3, 10):
From eb26157e666bc742035510c93376063e73b7e487 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Fri, 5 Apr 2024 14:10:47 -0700
Subject: [PATCH 012/101] only run for 3.8+
---
.github/workflows/ci_e2e_workflow.yml | 6 ++++--
.github/workflows/ci_ut_workflow.yml | 6 ++++--
azure_functions_worker/bindings/meta.py | 14 +++++++------
azure_functions_worker/constants.py | 6 +++++-
azure_functions_worker/dispatcher.py | 28 ++++++++++++++++---------
5 files changed, 39 insertions(+), 21 deletions(-)
diff --git a/.github/workflows/ci_e2e_workflow.yml b/.github/workflows/ci_e2e_workflow.yml
index 9e81be1d1..2ab4ea3d2 100644
--- a/.github/workflows/ci_e2e_workflow.yml
+++ b/.github/workflows/ci_e2e_workflow.yml
@@ -61,8 +61,10 @@ jobs:
python -m pip install --upgrade pip
python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre
python -m pip install -U -e .[dev]
- python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2]
-
+ # Conditionally install test dependencies for Python 3.8 and later
+ if [[ "${{ matrix.python-version }}" != "3.7" ]]; then
+ python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2]
+ fi
# Retry a couple times to avoid certificate issue
retry 5 python setup.py build
retry 5 python setup.py webhost --branch-name=dev
diff --git a/.github/workflows/ci_ut_workflow.yml b/.github/workflows/ci_ut_workflow.yml
index 1e3547955..9b5edec1a 100644
--- a/.github/workflows/ci_ut_workflow.yml
+++ b/.github/workflows/ci_ut_workflow.yml
@@ -59,8 +59,10 @@ jobs:
python -m pip install --upgrade pip
python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre
python -m pip install -U -e .[dev]
- python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2]
-
+ # Conditionally install test dependencies for Python 3.8 and later
+ if [[ "${{ matrix.python-version }}" != "3.7" ]]; then
+ python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2]
+ fi
# Retry a couple times to avoid certificate issue
retry 5 python setup.py build
retry 5 python setup.py webhost --branch-name=dev
diff --git a/azure_functions_worker/bindings/meta.py b/azure_functions_worker/bindings/meta.py
index 60d22a7c2..ab8fa8666 100644
--- a/azure_functions_worker/bindings/meta.py
+++ b/azure_functions_worker/bindings/meta.py
@@ -17,18 +17,20 @@
def _check_http_input_type_annotation(bind_name: str, pytype: type) -> bool:
- ext_base = sys.modules.get('azure.functions.extension.base')
- if ext_base is not None and ext_base.HttpV2FeatureChecker.http_v2_enabled():
- return ext_base.RequestTrackerMeta.check_type(pytype)
+ if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION:
+ ext_base = sys.modules.get('azure.functions.extension.base')
+ if ext_base is not None and ext_base.HttpV2FeatureChecker.http_v2_enabled():
+ return ext_base.RequestTrackerMeta.check_type(pytype)
binding = get_binding(bind_name)
return binding.check_input_type_annotation(pytype)
def _check_http_output_type_annotation(bind_name: str, pytype: type) -> bool:
- ext_base = sys.modules.get('azure.functions.extension.base')
- if ext_base is not None and ext_base.HttpV2FeatureChecker.http_v2_enabled():
- return ext_base.ResponseTrackerMeta.check_type(pytype)
+ if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION:
+ ext_base = sys.modules.get('azure.functions.extension.base')
+ if ext_base is not None and ext_base.HttpV2FeatureChecker.http_v2_enabled():
+ return ext_base.ResponseTrackerMeta.check_type(pytype)
binding = get_binding(bind_name)
return binding.check_output_type_annotation(pytype)
diff --git a/azure_functions_worker/constants.py b/azure_functions_worker/constants.py
index b38794017..eea0193ec 100644
--- a/azure_functions_worker/constants.py
+++ b/azure_functions_worker/constants.py
@@ -55,7 +55,8 @@
RETRY_POLICY = "retry_policy"
# Paths
-CUSTOMER_PACKAGES_PATH = "/home/site/wwwroot/.python_packages/lib/site-packages"
+CUSTOMER_PACKAGES_PATH = "/home/site/wwwroot/.python_packages/lib/site" \
+ "-packages"
# Flag to index functions in handle init request
PYTHON_ENABLE_INIT_INDEXING = "PYTHON_ENABLE_INIT_INDEXING"
@@ -73,3 +74,6 @@
# Output Names
HTTP = "http"
+
+# Base extension supported Python minor version
+BASE_EXT_SUPPORTED_PY_MINOR_VERSION = 8
diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py
index a2078f128..f6881cb49 100644
--- a/azure_functions_worker/dispatcher.py
+++ b/azure_functions_worker/dispatcher.py
@@ -33,7 +33,8 @@
PYTHON_SCRIPT_FILE_NAME_DEFAULT,
PYTHON_LANGUAGE_RUNTIME, PYTHON_ENABLE_INIT_INDEXING,
X_MS_INVOCATION_ID, LOCAL_HOST,
- METADATA_PROPERTIES_WORKER_INDEXED)
+ METADATA_PROPERTIES_WORKER_INDEXED,
+ BASE_EXT_SUPPORTED_PY_MINOR_VERSION)
from .extension import ExtensionManager
from .http_v2 import http_coordinator
from .logging import disable_console_logging, enable_console_logging
@@ -129,7 +130,8 @@ def get_sync_tp_workers_set(self):
3.9 scenarios (as we'll start passing only None by default), and we
need to get that information.
- Ref: concurrent.futures.thread.ThreadPoolExecutor.__init__._max_workers
+ Ref: concurrent.futures.thread.ThreadPoolExecutor.__init__
+ ._max_workers
"""
return self._sync_call_tp._max_workers
@@ -208,7 +210,8 @@ def stop(self) -> None:
self._stop_sync_call_tp()
- def on_logging(self, record: logging.LogRecord, formatted_msg: str) -> None:
+ def on_logging(self, record: logging.LogRecord,
+ formatted_msg: str) -> None:
if record.levelno >= logging.CRITICAL:
log_level = protos.RpcLog.Critical
elif record.levelno >= logging.ERROR:
@@ -506,7 +509,6 @@ async def _handle__function_load_request(self, request):
status=protos.StatusResult.Success)))
except Exception as ex:
- logging.error(ex)
return protos.StreamingMessage(
request_id=self.request_id,
function_load_response=protos.FunctionLoadResponse(
@@ -564,7 +566,9 @@ async def _handle__invocation_request(self, request):
shmem_mgr=self._shmem_mgr)
http_v2_enabled = False
- if fi.trigger_metadata.get('type') == HTTP_TRIGGER:
+ if sys.version_info.minor >= \
+ BASE_EXT_SUPPORTED_PY_MINOR_VERSION \
+ and fi.trigger_metadata.get('type') == HTTP_TRIGGER:
from azure.functions.extension.base import HttpV2FeatureChecker
http_v2_enabled = HttpV2FeatureChecker.http_v2_enabled()
@@ -578,10 +582,11 @@ async def _handle__invocation_request(self, request):
'Headers', 'Query']}
(RequestTrackerMeta.get_synchronizer()
- .sync_route_params(http_request, route_params))
+ .sync_route_params(http_request, route_params))
args[fi.trigger_metadata.get('param_name')] = http_request
- fi_context = self._get_context(invoc_request, fi.name, fi.directory)
+ fi_context = self._get_context(invoc_request, fi.name,
+ fi.directory)
# Use local thread storage to store the invocation ID
# for a customer's threads
@@ -721,7 +726,9 @@ async def _handle__function_environment_reload_request(self, request):
except Exception as ex:
self._function_metadata_exception = ex
- if self._has_http_func:
+ if sys.version_info.minor >= \
+ BASE_EXT_SUPPORTED_PY_MINOR_VERSION and \
+ self._has_http_func:
from azure.functions.extension.base \
import HttpV2FeatureChecker
@@ -810,7 +817,7 @@ def index_functions(self, function_path: str):
indexed_function_logs: List[str] = []
for func in indexed_functions:
self._has_http_func = self._has_http_func or \
- func.is_http_function()
+ func.is_http_function()
function_log = "Function Name: {}, Function Binding: {}" \
.format(func.get_function_name(),
[(binding.type, binding.name) for binding in
@@ -861,7 +868,8 @@ async def _handle__close_shared_memory_resources_request(self, request):
@staticmethod
def _get_context(invoc_request: protos.InvocationRequest, name: str,
directory: str) -> bindings.Context:
- """ For more information refer: https://aka.ms/azfunc-invocation-context
+ """ For more information refer:
+ https://aka.ms/azfunc-invocation-context
"""
trace_context = bindings.TraceContext(
invoc_request.trace_context.trace_parent,
From 2b99faa005f0903e6bd7727a81fd563dbbc09054 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Fri, 5 Apr 2024 14:13:27 -0700
Subject: [PATCH 013/101] FIX
---
.github/workflows/ci_consumption_workflow.yml | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/ci_consumption_workflow.yml b/.github/workflows/ci_consumption_workflow.yml
index e62d6b1b6..e1af76497 100644
--- a/.github/workflows/ci_consumption_workflow.yml
+++ b/.github/workflows/ci_consumption_workflow.yml
@@ -34,7 +34,9 @@ jobs:
run: |
python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre
python -m pip install -U -e .[dev]
- python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2]
+ if [[ "${{ matrix.python-version }}" != "3.7" ]]; then
+ python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2]
+ fi
python setup.py build
- name: Running 3.7 Tests
if: matrix.python-version == 3.7
From 5d255104d2d1188f8132dde2b43fc5c4a60174ee Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Fri, 5 Apr 2024 14:23:59 -0700
Subject: [PATCH 014/101] skip tests for 3.7-
---
tests/endtoend/test_http_functions.py | 3 +++
tests/unittests/test_http_functions.py | 10 ++++++++++
tests/unittests/test_http_functions_v2.py | 2 ++
tests/unittests/test_http_v2.py | 2 ++
4 files changed, 17 insertions(+)
diff --git a/tests/endtoend/test_http_functions.py b/tests/endtoend/test_http_functions.py
index 8d7061878..1d4abf44e 100644
--- a/tests/endtoend/test_http_functions.py
+++ b/tests/endtoend/test_http_functions.py
@@ -2,7 +2,9 @@
# Licensed under the MIT License.
import concurrent
import os
+import sys
import typing
+import unittest
from concurrent.futures import ThreadPoolExecutor
from unittest.mock import patch
@@ -222,6 +224,7 @@ def tearDownClass(cls):
super().tearDownClass()
+@unittest.skipIf(sys.version_info <= (3, 7), "Skipping tests if <= Python 3.7")
class TestHttpFunctionsV2FastApiWithInitIndexing(TestHttpFunctionsWithInitIndexing):
@classmethod
def get_script_dir(cls):
diff --git a/tests/unittests/test_http_functions.py b/tests/unittests/test_http_functions.py
index 7ccd55101..344ffdce8 100644
--- a/tests/unittests/test_http_functions.py
+++ b/tests/unittests/test_http_functions.py
@@ -108,6 +108,11 @@ def check_log_debug_logging(self, host_out: typing.List[str]):
self.assertIn('logging error', host_out)
self.assertNotIn('logging debug', host_out)
+ def test_debug_with_user_logging(self):
+ r = self.webhost.request('GET', 'debug_user_logging')
+ self.assertEqual(r.status_code, 200)
+ self.assertEqual(r.text, 'OK-user-debug')
+
def check_log_debug_with_user_logging(self, host_out: typing.List[str]):
self.assertIn('logging info', host_out)
self.assertIn('logging warning', host_out)
@@ -457,3 +462,8 @@ def test_no_return_returns(self):
r = self.webhost.request('GET', 'no_return_returns')
self.assertEqual(r.status_code, 200)
+class TestHttpFunctionsV2(TestHttpFunctions):
+ @classmethod
+ def get_script_dir(cls):
+ return testutils.UNIT_TESTS_FOLDER / 'http_functions' / \
+ 'http_v2_functions'
diff --git a/tests/unittests/test_http_functions_v2.py b/tests/unittests/test_http_functions_v2.py
index 7c5adcd5d..eb6951f50 100644
--- a/tests/unittests/test_http_functions_v2.py
+++ b/tests/unittests/test_http_functions_v2.py
@@ -6,6 +6,7 @@
import pathlib
import sys
import typing
+import unittest
from unittest import skipIf
from unittest.mock import patch
@@ -13,6 +14,7 @@
from tests.utils import testutils
+@unittest.skipIf(sys.version_info <= (3, 7), "Skipping tests if <= Python 3.7")
class TestHttpFunctionsV2FastApi(testutils.WebHostTestCase):
@classmethod
def setUpClass(cls):
diff --git a/tests/unittests/test_http_v2.py b/tests/unittests/test_http_v2.py
index 64aefe56f..048f84095 100644
--- a/tests/unittests/test_http_v2.py
+++ b/tests/unittests/test_http_v2.py
@@ -1,4 +1,5 @@
import asyncio
+import sys
import unittest
from unittest.mock import MagicMock
@@ -13,6 +14,7 @@ class MockHttpResponse:
pass
+@unittest.skipIf(sys.version_info <= (3, 7), "Skipping tests if <= Python 3.7")
class TestHttpCoordinator(unittest.TestCase):
def setUp(self):
self.invoc_id = "test_invocation"
From 4cea83a1b206b179af73a398189ce0f889871851 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Fri, 5 Apr 2024 15:59:18 -0700
Subject: [PATCH 015/101] fix
---
azure_functions_worker/bindings/meta.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/azure_functions_worker/bindings/meta.py b/azure_functions_worker/bindings/meta.py
index ab8fa8666..286e1dd72 100644
--- a/azure_functions_worker/bindings/meta.py
+++ b/azure_functions_worker/bindings/meta.py
@@ -9,6 +9,7 @@
from . import datumdef
from . import generic
from .shared_memory_data_transfer import SharedMemoryManager
+from ..constants import BASE_EXT_SUPPORTED_PY_MINOR_VERSION
PB_TYPE = 'rpc_data'
PB_TYPE_DATA = 'data'
From a9414d86528c0d6c376dc18627b271ea57f87456 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Fri, 5 Apr 2024 16:16:11 -0700
Subject: [PATCH 016/101] fix
---
.github/workflows/ci_consumption_workflow.yml | 1 -
.github/workflows/ci_e2e_workflow.yml | 1 -
2 files changed, 2 deletions(-)
diff --git a/.github/workflows/ci_consumption_workflow.yml b/.github/workflows/ci_consumption_workflow.yml
index e1af76497..172e19f7e 100644
--- a/.github/workflows/ci_consumption_workflow.yml
+++ b/.github/workflows/ci_consumption_workflow.yml
@@ -12,7 +12,6 @@ on:
push:
branches: [ dev, main, release/* ]
pull_request:
- branches: [ dev, main, release/* ]
jobs:
build:
diff --git a/.github/workflows/ci_e2e_workflow.yml b/.github/workflows/ci_e2e_workflow.yml
index 2ab4ea3d2..6cbff4704 100644
--- a/.github/workflows/ci_e2e_workflow.yml
+++ b/.github/workflows/ci_e2e_workflow.yml
@@ -13,7 +13,6 @@ on:
push:
branches: [dev, main, release/*]
pull_request:
- branches: [dev, main, release/*]
schedule:
# Monday to Thursday 3 AM CST build
# * is a special character in YAML so you have to quote this string
From e20b8bb1e96163fca9df606542f2db23eb20e4eb Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Fri, 5 Apr 2024 16:18:13 -0700
Subject: [PATCH 017/101] fix
---
.github/workflows/ci_ut_workflow.yml | 2 +-
.github/workflows/linter.yml | 7 +++++--
2 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/ci_ut_workflow.yml b/.github/workflows/ci_ut_workflow.yml
index 9b5edec1a..1c3777e7a 100644
--- a/.github/workflows/ci_ut_workflow.yml
+++ b/.github/workflows/ci_ut_workflow.yml
@@ -15,8 +15,8 @@ on:
# * is a special character in YAML so you have to quote this string
- cron: "0 8 * * 1,2,3,4"
push:
- pull_request:
branches: [ dev, main, release/* ]
+ pull_request:
jobs:
build:
diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml
index 83e6f572f..d0923a8d5 100644
--- a/.github/workflows/linter.yml
+++ b/.github/workflows/linter.yml
@@ -15,8 +15,11 @@ name: Lint Code Base
#############################
# Start the job on all push #
#############################
-on: [ push, pull_request, workflow_dispatch ]
-
+on:
+ workflow_dispatch:
+ push:
+ branches: [ dev, main, release/* ]
+ pull_request:
###############
# Set the Job #
###############
From 64d9e18defb4ac1d4ca2e979a7dfed0be363663a Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Fri, 5 Apr 2024 16:38:15 -0700
Subject: [PATCH 018/101] fix
---
azure_functions_worker/bindings/meta.py | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/azure_functions_worker/bindings/meta.py b/azure_functions_worker/bindings/meta.py
index 286e1dd72..097d00d94 100644
--- a/azure_functions_worker/bindings/meta.py
+++ b/azure_functions_worker/bindings/meta.py
@@ -20,7 +20,8 @@
def _check_http_input_type_annotation(bind_name: str, pytype: type) -> bool:
if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION:
ext_base = sys.modules.get('azure.functions.extension.base')
- if ext_base is not None and ext_base.HttpV2FeatureChecker.http_v2_enabled():
+ if ext_base is not None and \
+ ext_base.HttpV2FeatureChecker.http_v2_enabled():
return ext_base.RequestTrackerMeta.check_type(pytype)
binding = get_binding(bind_name)
@@ -30,7 +31,8 @@ def _check_http_input_type_annotation(bind_name: str, pytype: type) -> bool:
def _check_http_output_type_annotation(bind_name: str, pytype: type) -> bool:
if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION:
ext_base = sys.modules.get('azure.functions.extension.base')
- if ext_base is not None and ext_base.HttpV2FeatureChecker.http_v2_enabled():
+ if ext_base is not None and \
+ ext_base.HttpV2FeatureChecker.http_v2_enabled():
return ext_base.ResponseTrackerMeta.check_type(pytype)
binding = get_binding(bind_name)
From f1c780a42180dd77ae7c9015b77d375032262cc4 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Fri, 5 Apr 2024 17:02:56 -0700
Subject: [PATCH 019/101] fix styles
---
azure_functions_worker/dispatcher.py | 2 +-
.../test_linux_consumption.py | 13 +-
.../function_app.py | 6 +-
.../get_eventhub_batch_triggered/__init__.py | 3 +-
.../fastapi/file_name/main.py | 4 +-
.../http_functions_v2/fastapi/function_app.py | 13 +-
tests/endtoend/test_http_functions.py | 275 +++++++++---------
tests/endtoend/test_servicebus_functions.py | 22 +-
.../http_v2_functions/fastapi/function_app.py | 9 +-
tests/unittests/test_dispatcher.py | 42 +--
tests/unittests/test_http_functions.py | 2 +
tests/unittests/test_http_functions_v2.py | 6 +-
tests/utils/testutils.py | 3 +-
13 files changed, 210 insertions(+), 190 deletions(-)
diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py
index f6881cb49..992c6c639 100644
--- a/azure_functions_worker/dispatcher.py
+++ b/azure_functions_worker/dispatcher.py
@@ -817,7 +817,7 @@ def index_functions(self, function_path: str):
indexed_function_logs: List[str] = []
for func in indexed_functions:
self._has_http_func = self._has_http_func or \
- func.is_http_function()
+ func.is_http_function()
function_log = "Function Name: {}, Function Binding: {}" \
.format(func.get_function_name(),
[(binding.type, binding.name) for binding in
diff --git a/tests/consumption_tests/test_linux_consumption.py b/tests/consumption_tests/test_linux_consumption.py
index 3c7232366..9c5be090f 100644
--- a/tests/consumption_tests/test_linux_consumption.py
+++ b/tests/consumption_tests/test_linux_consumption.py
@@ -342,12 +342,12 @@ def test_http_v2_fastapi_streaming_upload_download(self):
"""
A function app with init indexing enabled
"""
- import random as rand
with LinuxConsumptionWebHostController(_DEFAULT_HOST_VERSION,
self._py_version) as ctrl:
ctrl.assign_container(env={
"AzureWebJobsStorage": self._storage,
- "SCM_RUN_FROM_PACKAGE": self._get_blob_url("HttpV2FastApiStreaming"),
+ "SCM_RUN_FROM_PACKAGE":
+ self._get_blob_url("HttpV2FastApiStreaming"),
PYTHON_ENABLE_INIT_INDEXING: "true",
PYTHON_ISOLATE_WORKER_DEPENDENCIES: "1"
})
@@ -360,16 +360,19 @@ def generate_random_bytes_stream():
yield b'is'
yield b'returned'
- req = Request('POST', f'{ctrl.url}/api/http_v2_fastapi_streaming', data=generate_random_bytes_stream())
+ req = Request('POST',
+ f'{ctrl.url}/api/http_v2_fastapi_streaming',
+ data=generate_random_bytes_stream())
resp = ctrl.send_request(req)
self.assertEqual(resp.status_code, 200)
streamed_data = b''
for chunk in resp.iter_content(chunk_size=1024):
if chunk:
- streamed_data+= chunk
+ streamed_data += chunk
- self.assertEqual(streamed_data, b'streamingtestingresponseisreturned')
+ self.assertEqual(
+ streamed_data, b'streamingtestingresponseisreturned')
def _get_blob_url(self, scenario_name: str) -> str:
return (
diff --git a/tests/endtoend/eventhub_batch_functions/eventhub_batch_functions_stein/function_app.py b/tests/endtoend/eventhub_batch_functions/eventhub_batch_functions_stein/function_app.py
index 093d69228..30fe94cdf 100644
--- a/tests/endtoend/eventhub_batch_functions/eventhub_batch_functions_stein/function_app.py
+++ b/tests/endtoend/eventhub_batch_functions/eventhub_batch_functions_stein/function_app.py
@@ -50,8 +50,10 @@ def eventhub_output_batch(req: func.HttpRequest, out: func.Out[str]) -> str:
@app.blob_input(arg_name="testEntities",
path="python-worker-tests/test-eventhub-batch-triggered.txt",
connection="AzureWebJobsStorage")
-def get_eventhub_batch_triggered(req: func.HttpRequest, testEntities: func.InputStream):
- return func.HttpResponse(status_code=200, body=testEntities.read().decode('utf-8'))
+def get_eventhub_batch_triggered(req: func.HttpRequest,
+ testEntities: func.InputStream):
+ return func.HttpResponse(status_code=200,
+ body=testEntities.read().decode('utf-8'))
# Retrieve the event data from storage blob and return it as Http response
diff --git a/tests/endtoend/eventhub_batch_functions/get_eventhub_batch_triggered/__init__.py b/tests/endtoend/eventhub_batch_functions/get_eventhub_batch_triggered/__init__.py
index 153829b31..feca352fb 100644
--- a/tests/endtoend/eventhub_batch_functions/get_eventhub_batch_triggered/__init__.py
+++ b/tests/endtoend/eventhub_batch_functions/get_eventhub_batch_triggered/__init__.py
@@ -5,4 +5,5 @@
# Retrieve the event data from storage blob and return it as Http response
def main(req: func.HttpRequest, testEntities: func.InputStream):
- return func.HttpResponse(status_code=200, body=testEntities.read().decode('utf-8'))
+ return func.HttpResponse(status_code=200,
+ body=testEntities.read().decode('utf-8'))
diff --git a/tests/endtoend/http_functions/http_functions_v2/fastapi/file_name/main.py b/tests/endtoend/http_functions/http_functions_v2/fastapi/file_name/main.py
index ad2831f0a..c9718fef5 100644
--- a/tests/endtoend/http_functions/http_functions_v2/fastapi/file_name/main.py
+++ b/tests/endtoend/http_functions/http_functions_v2/fastapi/file_name/main.py
@@ -7,9 +7,7 @@
import azure.functions as func
-from azure.functions.extension.fastapi import Request, Response, StreamingResponse, \
- HTMLResponse, PlainTextResponse, HTMLResponse, JSONResponse, \
- UJSONResponse, ORJSONResponse, RedirectResponse, FileResponse
+from azure.functions.extension.fastapi import Request, Response
app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
diff --git a/tests/endtoend/http_functions/http_functions_v2/fastapi/function_app.py b/tests/endtoend/http_functions/http_functions_v2/fastapi/function_app.py
index 1870767da..b82e0baee 100644
--- a/tests/endtoend/http_functions/http_functions_v2/fastapi/function_app.py
+++ b/tests/endtoend/http_functions/http_functions_v2/fastapi/function_app.py
@@ -1,14 +1,13 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
-import asyncio
from datetime import datetime
import logging
import time
import azure.functions as func
-from azure.functions.extension.fastapi import Request, Response, StreamingResponse, \
- HTMLResponse, PlainTextResponse, HTMLResponse, JSONResponse, \
- UJSONResponse, ORJSONResponse, RedirectResponse, FileResponse
+from azure.functions.extension.fastapi import Request, Response, \
+ StreamingResponse, HTMLResponse, \
+ UJSONResponse, ORJSONResponse, FileResponse
app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
@@ -73,19 +72,23 @@ async def content():
yield b"Second chunk\n"
return StreamingResponse(content())
+
@app.route(route="return_html")
def return_html(req: Request) -> HTMLResponse:
html_content = "Hello, World!
"
return HTMLResponse(content=html_content, status_code=200)
+
@app.route(route="return_ujson")
def return_ujson(req: Request) -> UJSONResponse:
return UJSONResponse(content={"message": "Hello, World!"}, status_code=200)
+
@app.route(route="return_orjson")
def return_orjson(req: Request) -> ORJSONResponse:
return ORJSONResponse(content={"message": "Hello, World!"}, status_code=200)
+
@app.route(route="return_file")
def return_file(req: Request) -> FileResponse:
- return FileResponse("function_app.py")
\ No newline at end of file
+ return FileResponse("function_app.py")
diff --git a/tests/endtoend/test_http_functions.py b/tests/endtoend/test_http_functions.py
index 1d4abf44e..c12e91ab6 100644
--- a/tests/endtoend/test_http_functions.py
+++ b/tests/endtoend/test_http_functions.py
@@ -225,22 +225,43 @@ def tearDownClass(cls):
@unittest.skipIf(sys.version_info <= (3, 7), "Skipping tests if <= Python 3.7")
-class TestHttpFunctionsV2FastApiWithInitIndexing(TestHttpFunctionsWithInitIndexing):
- @classmethod
- def get_script_dir(cls):
- return testutils.E2E_TESTS_FOLDER / 'http_functions' / \
- 'http_functions_v2' / \
- 'fastapi'
-
- @testutils.retryable_test(3, 5)
- def test_return_streaming(self):
- """Test if the return_streaming function returns a streaming
- response"""
- root_url = self.webhost._addr
- streaming_url = f'{root_url}/api/return_streaming'
- r = requests.get(streaming_url, timeout=REQUEST_TIMEOUT_SEC, stream=True)
+class TestHttpFunctionsV2FastApiWithInitIndexing(
+ TestHttpFunctionsWithInitIndexing):
+ @classmethod
+ def get_script_dir(cls):
+ return testutils.E2E_TESTS_FOLDER / 'http_functions' / \
+ 'http_functions_v2' / \
+ 'fastapi'
+
+ @testutils.retryable_test(3, 5)
+ def test_return_streaming(self):
+ """Test if the return_streaming function returns a streaming
+ response"""
+ root_url = self.webhost._addr
+ streaming_url = f'{root_url}/api/return_streaming'
+ r = requests.get(
+ streaming_url, timeout=REQUEST_TIMEOUT_SEC, stream=True)
+ self.assertTrue(r.ok)
+ # Validate streaming content
+ expected_content = [b"First chunk\n", b"Second chunk\n"]
+ received_content = []
+ for chunk in r.iter_content(chunk_size=1024):
+ if chunk:
+ received_content.append(chunk)
+ self.assertEqual(received_content, expected_content)
+
+ @testutils.retryable_test(3, 5)
+ def test_return_streaming_concurrently(self):
+ """Test if the return_streaming function returns a streaming
+ response concurrently"""
+ root_url = self.webhost._addr
+ streaming_url = f'{root_url}/return_streaming'
+
+ # Function to make a streaming request and validate content
+ def make_request():
+ r = requests.get(streaming_url, timeout=REQUEST_TIMEOUT_SEC,
+ stream=True)
self.assertTrue(r.ok)
- # Validate streaming content
expected_content = [b"First chunk\n", b"Second chunk\n"]
received_content = []
for chunk in r.iter_content(chunk_size=1024):
@@ -248,134 +269,116 @@ def test_return_streaming(self):
received_content.append(chunk)
self.assertEqual(received_content, expected_content)
- @testutils.retryable_test(3, 5)
- def test_return_streaming_concurrently(self):
- """Test if the return_streaming function returns a streaming
- response concurrently"""
- root_url = self.webhost._addr
- streaming_url = f'{root_url}/return_streaming'
-
- # Function to make a streaming request and validate content
- def make_request():
- r = requests.get(streaming_url, timeout=REQUEST_TIMEOUT_SEC,
- stream=True)
- self.assertTrue(r.ok)
- expected_content = [b"First chunk\n", b"Second chunk\n"]
- received_content = []
- for chunk in r.iter_content(chunk_size=1024):
- if chunk:
- received_content.append(chunk)
- self.assertEqual(received_content, expected_content)
-
- # Make concurrent requests
- with ThreadPoolExecutor(max_workers=2) as executor:
- executor.map(make_request, range(2))
-
- @testutils.retryable_test(3, 5)
- def test_return_html(self):
- """Test if the return_html function returns an HTML response"""
- root_url = self.webhost._addr
- html_url = f'{root_url}/api/return_html'
- r = requests.get(html_url, timeout=REQUEST_TIMEOUT_SEC)
- self.assertTrue(r.ok)
- self.assertEqual(r.headers['content-type'],
- 'text/html; charset=utf-8')
- # Validate HTML content
- expected_html = "Hello, World!
"
- self.assertEqual(r.text, expected_html)
-
- @testutils.retryable_test(3, 5)
- def test_return_ujson(self):
- """Test if the return_ujson function returns a UJSON response"""
- root_url = self.webhost._addr
- ujson_url = f'{root_url}/api/return_ujson'
- r = requests.get(ujson_url, timeout=REQUEST_TIMEOUT_SEC)
- self.assertTrue(r.ok)
- self.assertEqual(r.headers['content-type'],'application/json')
- self.assertEqual(r.text, '{"message":"Hello, World!"}')
-
- @testutils.retryable_test(3, 5)
- def test_return_orjson(self):
- """Test if the return_orjson function returns an ORJSON response"""
- root_url = self.webhost._addr
- orjson_url = f'{root_url}/api/return_orjson'
- r = requests.get(orjson_url, timeout=REQUEST_TIMEOUT_SEC)
- self.assertTrue(r.ok)
- self.assertEqual(r.headers['content-type'], 'application/json')
- self.assertEqual(r.text, '{"message":"Hello, World!"}')
-
- @testutils.retryable_test(3, 5)
- def test_return_file(self):
- """Test if the return_file function returns a file response"""
- root_url = self.webhost._addr
- file_url = f'{root_url}/api/return_file'
- r = requests.get(file_url, timeout=REQUEST_TIMEOUT_SEC)
- self.assertTrue(r.ok)
- self.assertIn('@app.route(route="default_template")', r.text)
+ # Make concurrent requests
+ with ThreadPoolExecutor(max_workers=2) as executor:
+ executor.map(make_request, range(2))
- @testutils.retryable_test(3, 5)
- def test_upload_data_stream(self):
- """Test if the upload_data_stream function receives streaming data
- and returns the complete data"""
- root_url = self.webhost._addr
- upload_url = f'{root_url}/api/upload_data_stream'
+ @testutils.retryable_test(3, 5)
+ def test_return_html(self):
+ """Test if the return_html function returns an HTML response"""
+ root_url = self.webhost._addr
+ html_url = f'{root_url}/api/return_html'
+ r = requests.get(html_url, timeout=REQUEST_TIMEOUT_SEC)
+ self.assertTrue(r.ok)
+ self.assertEqual(r.headers['content-type'],
+ 'text/html; charset=utf-8')
+ # Validate HTML content
+ expected_html = "Hello, World!
"
+ self.assertEqual(r.text, expected_html)
- # Define the streaming data
- data_chunks = [b"First chunk\n", b"Second chunk\n"]
+ @testutils.retryable_test(3, 5)
+ def test_return_ujson(self):
+ """Test if the return_ujson function returns a UJSON response"""
+ root_url = self.webhost._addr
+ ujson_url = f'{root_url}/api/return_ujson'
+ r = requests.get(ujson_url, timeout=REQUEST_TIMEOUT_SEC)
+ self.assertTrue(r.ok)
+ self.assertEqual(r.headers['content-type'], 'application/json')
+ self.assertEqual(r.text, '{"message":"Hello, World!"}')
- # Define a function to simulate streaming by reading from an
- # iterator
- def stream_data(data_chunks):
- for chunk in data_chunks:
- yield chunk
+ @testutils.retryable_test(3, 5)
+ def test_return_orjson(self):
+ """Test if the return_orjson function returns an ORJSON response"""
+ root_url = self.webhost._addr
+ orjson_url = f'{root_url}/api/return_orjson'
+ r = requests.get(orjson_url, timeout=REQUEST_TIMEOUT_SEC)
+ self.assertTrue(r.ok)
+ self.assertEqual(r.headers['content-type'], 'application/json')
+ self.assertEqual(r.text, '{"message":"Hello, World!"}')
- # Send a POST request with streaming data
- r = requests.post(upload_url, data=stream_data(data_chunks))
+ @testutils.retryable_test(3, 5)
+ def test_return_file(self):
+ """Test if the return_file function returns a file response"""
+ root_url = self.webhost._addr
+ file_url = f'{root_url}/api/return_file'
+ r = requests.get(file_url, timeout=REQUEST_TIMEOUT_SEC)
+ self.assertTrue(r.ok)
+ self.assertIn('@app.route(route="default_template")', r.text)
- # Assert that the request was successful
- self.assertTrue(r.ok)
+ @testutils.retryable_test(3, 5)
+ def test_upload_data_stream(self):
+ """Test if the upload_data_stream function receives streaming data
+ and returns the complete data"""
+ root_url = self.webhost._addr
+ upload_url = f'{root_url}/api/upload_data_stream'
+
+ # Define the streaming data
+ data_chunks = [b"First chunk\n", b"Second chunk\n"]
+
+ # Define a function to simulate streaming by reading from an
+ # iterator
+ def stream_data(data_chunks):
+ for chunk in data_chunks:
+ yield chunk
+
+ # Send a POST request with streaming data
+ r = requests.post(upload_url, data=stream_data(data_chunks))
- # Assert that the response content matches the concatenation of
- # all data chunks
+ # Assert that the request was successful
+ self.assertTrue(r.ok)
+
+ # Assert that the response content matches the concatenation of
+ # all data chunks
+ complete_data = b"".join(data_chunks)
+ self.assertEqual(r.content, complete_data)
+
+ @testutils.retryable_test(3, 5)
+ def test_upload_data_stream_concurrently(self):
+ """Test if the upload_data_stream function receives streaming data
+ and returns the complete data"""
+ root_url = self.webhost._addr
+ upload_url = f'{root_url}/api/upload_data_stream'
+
+ # Define the streaming data
+ data_chunks = [b"First chunk\n", b"Second chunk\n"]
+
+ # Define a function to simulate streaming by reading from an
+ # iterator
+ def stream_data(data_chunks):
+ for chunk in data_chunks:
+ yield chunk
+
+ # Define the number of concurrent requests
+ num_requests = 5
+
+ # Define a function to send a single request
+ def send_request():
+ r = requests.post(upload_url, data=stream_data(data_chunks))
+ return r.ok, r.content
+
+ # Send multiple requests concurrently
+ with concurrent.futures.ThreadPoolExecutor() as executor:
+ futures = [executor.submit(send_request) for _ in
+ range(num_requests)]
+
+ # Assert that all requests were successful and the response
+ # contents are correct
+ for future in concurrent.futures.as_completed(futures):
+ ok, content = future.result()
+ self.assertTrue(ok)
complete_data = b"".join(data_chunks)
- self.assertEqual(r.content, complete_data)
-
- @testutils.retryable_test(3, 5)
- def test_upload_data_stream_concurrently(self):
- """Test if the upload_data_stream function receives streaming data
- and returns the complete data"""
- root_url = self.webhost._addr
- upload_url = f'{root_url}/api/upload_data_stream'
-
- # Define the streaming data
- data_chunks = [b"First chunk\n", b"Second chunk\n"]
-
- # Define a function to simulate streaming by reading from an
- # iterator
- def stream_data(data_chunks):
- for chunk in data_chunks:
- yield chunk
-
- # Define the number of concurrent requests
- num_requests = 5
-
- # Define a function to send a single request
- def send_request():
- r = requests.post(upload_url, data=stream_data(data_chunks))
- return r.ok, r.content
-
- # Send multiple requests concurrently
- with concurrent.futures.ThreadPoolExecutor() as executor:
- futures = [executor.submit(send_request) for _ in
- range(num_requests)]
-
- # Assert that all requests were successful and the response
- # contents are correct
- for future in concurrent.futures.as_completed(futures):
- ok, content = future.result()
- self.assertTrue(ok)
- complete_data = b"".join(data_chunks)
- self.assertEqual(content, complete_data)
+ self.assertEqual(content, complete_data)
+
class TestUserThreadLoggingHttpFunctions(testutils.WebHostTestCase):
"""Test the Http trigger that contains logging with user threads.
diff --git a/tests/endtoend/test_servicebus_functions.py b/tests/endtoend/test_servicebus_functions.py
index 34f51c5bc..48d71392e 100644
--- a/tests/endtoend/test_servicebus_functions.py
+++ b/tests/endtoend/test_servicebus_functions.py
@@ -37,16 +37,18 @@ def test_servicebus_basic(self):
self.assertEqual(r.status_code, 200)
msg = r.json()
self.assertEqual(msg['body'], data)
- for attr in {'message_id', 'body', 'content_type', 'delivery_count',
- 'expiration_time', 'label', 'partition_key', 'reply_to',
- 'reply_to_session_id', 'scheduled_enqueue_time',
- 'session_id', 'time_to_live', 'to', 'user_properties',
- 'application_properties', 'correlation_id',
- 'dead_letter_error_description', 'dead_letter_reason',
- 'dead_letter_source', 'enqueued_sequence_number',
- 'enqueued_time_utc', 'expires_at_utc', 'locked_until',
- 'lock_token', 'sequence_number', 'state', 'subject',
- 'transaction_partition_key'}:
+ for attr in {
+ 'message_id', 'body', 'content_type', 'delivery_count',
+ 'expiration_time', 'label', 'partition_key', 'reply_to',
+ 'reply_to_session_id', 'scheduled_enqueue_time',
+ 'session_id', 'time_to_live', 'to', 'user_properties',
+ 'application_properties', 'correlation_id',
+ 'dead_letter_error_description', 'dead_letter_reason',
+ 'dead_letter_source', 'enqueued_sequence_number',
+ 'enqueued_time_utc', 'expires_at_utc', 'locked_until',
+ 'lock_token', 'sequence_number', 'state', 'subject',
+ 'transaction_partition_key'
+ }:
self.assertIn(attr, msg)
except (AssertionError, json.JSONDecodeError):
if try_no == max_retries - 1:
diff --git a/tests/unittests/http_functions/http_v2_functions/fastapi/function_app.py b/tests/unittests/http_functions/http_v2_functions/fastapi/function_app.py
index 25accf853..9830f572e 100644
--- a/tests/unittests/http_functions/http_v2_functions/fastapi/function_app.py
+++ b/tests/unittests/http_functions/http_v2_functions/fastapi/function_app.py
@@ -2,13 +2,12 @@
# Licensed under the MIT License.
import asyncio
import hashlib
-import json
import logging
import sys
import time
from urllib.request import urlopen
from azure.functions.extension.fastapi import Request, Response, \
- PlainTextResponse, HTMLResponse, RedirectResponse
+ HTMLResponse, RedirectResponse
import azure.functions as func
from pydantic import BaseModel
@@ -273,11 +272,11 @@ def return_http_redirect(req: Request):
async def return_request(req: Request):
params = dict(req.query_params)
params.pop('code', None) # Remove 'code' parameter if present
-
+
# Get the body content and calculate its hash
body = await req.body()
body_hash = hashlib.sha256(body).hexdigest() if body else None
-
+
# Return a dictionary containing request information
return {
'method': req.method,
@@ -431,4 +430,4 @@ def set_cookie_resp_header_default_values(
value='42'
)
- return resp
\ No newline at end of file
+ return resp
diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py
index 37f23ea5f..ba9670e24 100644
--- a/tests/unittests/test_dispatcher.py
+++ b/tests/unittests/test_dispatcher.py
@@ -9,13 +9,14 @@
from unittest.mock import patch
from azure_functions_worker import protos
-from azure_functions_worker.constants import (PYTHON_THREADPOOL_THREAD_COUNT,
- PYTHON_THREADPOOL_THREAD_COUNT_DEFAULT,
- PYTHON_THREADPOOL_THREAD_COUNT_MAX_37,
- PYTHON_THREADPOOL_THREAD_COUNT_MIN,
- PYTHON_ENABLE_INIT_INDEXING,
- METADATA_PROPERTIES_WORKER_INDEXED,
- PYTHON_ENABLE_DEBUG_LOGGING)
+from azure_functions_worker.constants import (
+ PYTHON_THREADPOOL_THREAD_COUNT,
+ PYTHON_THREADPOOL_THREAD_COUNT_DEFAULT,
+ PYTHON_THREADPOOL_THREAD_COUNT_MAX_37,
+ PYTHON_THREADPOOL_THREAD_COUNT_MIN,
+ PYTHON_ENABLE_INIT_INDEXING,
+ METADATA_PROPERTIES_WORKER_INDEXED,
+ PYTHON_ENABLE_DEBUG_LOGGING)
from azure_functions_worker.dispatcher import Dispatcher
from azure_functions_worker.version import VERSION
from tests.utils import testutils
@@ -682,9 +683,11 @@ async def test_dispatcher_load_azfunc_in_init(self):
1
)
self.assertEqual(
- len([log for log in r.logs if log.message.startswith(
- "Received WorkerMetadataRequest from _handle__worker_init_request"
- )]),
+ len([log for log in r.logs if
+ log.message.startswith(
+ "Received WorkerMetadataRequest from"
+ "_handle__worker_init_request"
+ )]),
0
)
self.assertIn("azure.functions", sys.modules)
@@ -844,11 +847,14 @@ def test_functions_metadata_request_with_init_indexing_enabled(self):
self.assertEqual(init_response.worker_init_response.result.status,
protos.StatusResult.Success)
- metadata_response = self.loop.run_until_complete(
- self.dispatcher._handle__functions_metadata_request(metadata_request))
+ metadata_response = \
+ self.loop.run_until_complete(
+ self.dispatcher._handle__functions_metadata_request(
+ metadata_request))
- self.assertEqual(metadata_response.function_metadata_response.result.status,
- protos.StatusResult.Success)
+ self.assertEqual(
+ metadata_response.function_metadata_response.result.status,
+ protos.StatusResult.Success)
self.assertIsNotNone(self.dispatcher._function_metadata_result)
self.assertIsNone(self.dispatcher._function_metadata_exception)
@@ -875,10 +881,12 @@ def test_functions_metadata_request_with_init_indexing_disabled(self):
self.assertIsNone(self.dispatcher._function_metadata_exception)
metadata_response = self.loop.run_until_complete(
- self.dispatcher._handle__functions_metadata_request(metadata_request))
+ self.dispatcher._handle__functions_metadata_request(
+ metadata_request))
- self.assertEqual(metadata_response.function_metadata_response.result.status,
- protos.StatusResult.Success)
+ self.assertEqual(
+ metadata_response.function_metadata_response.result.status,
+ protos.StatusResult.Success)
self.assertIsNotNone(self.dispatcher._function_metadata_result)
self.assertIsNone(self.dispatcher._function_metadata_exception)
diff --git a/tests/unittests/test_http_functions.py b/tests/unittests/test_http_functions.py
index 344ffdce8..f21a5aff9 100644
--- a/tests/unittests/test_http_functions.py
+++ b/tests/unittests/test_http_functions.py
@@ -10,6 +10,7 @@
from tests.utils import testutils
+
class TestHttpFunctions(testutils.WebHostTestCase):
@classmethod
@@ -462,6 +463,7 @@ def test_no_return_returns(self):
r = self.webhost.request('GET', 'no_return_returns')
self.assertEqual(r.status_code, 200)
+
class TestHttpFunctionsV2(TestHttpFunctions):
@classmethod
def get_script_dir(cls):
diff --git a/tests/unittests/test_http_functions_v2.py b/tests/unittests/test_http_functions_v2.py
index eb6951f50..202dce14d 100644
--- a/tests/unittests/test_http_functions_v2.py
+++ b/tests/unittests/test_http_functions_v2.py
@@ -35,7 +35,7 @@ def tearDownClass(cls):
def get_script_dir(cls):
return testutils.UNIT_TESTS_FOLDER / 'http_functions' / \
'http_v2_functions' / \
- 'fastapi'
+ 'fastapi'
def test_return_bytes(self):
r = self.webhost.request('GET', 'return_bytes')
@@ -372,7 +372,6 @@ def test_response_cookie_header_nullable_timestamp_err(self):
'response_cookie_header_nullable_timestamp_err')
self.assertEqual(r.status_code, 200)
-
@skipIf(sys.version_info < (3, 8, 0),
"Skip the tests for Python 3.7 and below")
def test_response_cookie_header_nullable_bool_err(self):
@@ -382,7 +381,6 @@ def test_response_cookie_header_nullable_bool_err(self):
self.assertEqual(r.status_code, 200)
self.assertTrue("Set-Cookie" in r.headers)
-
def test_print_to_console_stderr(self):
r = self.webhost.request('GET', 'print_logging?console=true'
'&message=Secret42&is_stderr=true')
@@ -456,4 +454,4 @@ def check_return_pydantic_model_with_missing_fields(self,
host_out:
typing.List[str]):
self.assertIn("Field required [type=missing, input_value={'name': "
- "'item1'}, input_type=dict]", host_out)
\ No newline at end of file
+ "'item1'}, input_type=dict]", host_out)
diff --git a/tests/utils/testutils.py b/tests/utils/testutils.py
index 57946f1eb..86467f0c7 100644
--- a/tests/utils/testutils.py
+++ b/tests/utils/testutils.py
@@ -249,7 +249,8 @@ def setUpClass(cls):
cls.host_stdout_logger.error(error_message)
raise RuntimeError(error_message)
except Exception as ex:
- cls.host_stdout_logger.error(f"WebHost is not started correctly. {ex}")
+ cls.host_stdout_logger.error(
+ f"WebHost is not started correctly. {ex}")
cls.tearDownClass()
raise
From bb8e77a4581ff8d577ea612b58d5070d19b200be Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Fri, 5 Apr 2024 17:44:51 -0700
Subject: [PATCH 020/101] fix
---
tests/unittests/test_http_functions.py | 9 +--------
1 file changed, 1 insertion(+), 8 deletions(-)
diff --git a/tests/unittests/test_http_functions.py b/tests/unittests/test_http_functions.py
index f21a5aff9..9819430d1 100644
--- a/tests/unittests/test_http_functions.py
+++ b/tests/unittests/test_http_functions.py
@@ -461,11 +461,4 @@ def test_no_return(self):
def test_no_return_returns(self):
r = self.webhost.request('GET', 'no_return_returns')
- self.assertEqual(r.status_code, 200)
-
-
-class TestHttpFunctionsV2(TestHttpFunctions):
- @classmethod
- def get_script_dir(cls):
- return testutils.UNIT_TESTS_FOLDER / 'http_functions' / \
- 'http_v2_functions'
+ self.assertEqual(r.status_code, 200)
\ No newline at end of file
From 1e66de647e1f769f9014b548ce77d0e1da76d9aa Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Fri, 5 Apr 2024 17:50:44 -0700
Subject: [PATCH 021/101] s
---
.../function_app.py | 23 ++++++++-------
.../get_eventhub_batch_triggered/__init__.py | 5 ++--
tests/endtoend/test_servicebus_functions.py | 28 +++++++------------
3 files changed, 23 insertions(+), 33 deletions(-)
diff --git a/tests/endtoend/eventhub_batch_functions/eventhub_batch_functions_stein/function_app.py b/tests/endtoend/eventhub_batch_functions/eventhub_batch_functions_stein/function_app.py
index 30fe94cdf..bccc29d27 100644
--- a/tests/endtoend/eventhub_batch_functions/eventhub_batch_functions_stein/function_app.py
+++ b/tests/endtoend/eventhub_batch_functions/eventhub_batch_functions_stein/function_app.py
@@ -18,10 +18,10 @@
connection="AzureWebJobsEventHubConnectionString",
data_type="string",
cardinality="many")
-@app.blob_output(arg_name="$return",
- path="python-worker-tests/test-eventhub-batch-triggered.txt",
- connection="AzureWebJobsStorage")
-def eventhub_multiple(events) -> str:
+@app.table_output(arg_name="$return",
+ connection="AzureWebJobsStorage",
+ table_name="EventHubBatchTest")
+def eventhub_multiple(events):
table_entries = []
for event in events:
json_entry = event.get_body()
@@ -46,14 +46,13 @@ def eventhub_output_batch(req: func.HttpRequest, out: func.Out[str]) -> str:
# Retrieve the event data from storage blob and return it as Http response
@app.function_name(name="get_eventhub_batch_triggered")
-@app.route(route="get_eventhub_batch_triggered")
-@app.blob_input(arg_name="testEntities",
- path="python-worker-tests/test-eventhub-batch-triggered.txt",
- connection="AzureWebJobsStorage")
-def get_eventhub_batch_triggered(req: func.HttpRequest,
- testEntities: func.InputStream):
- return func.HttpResponse(status_code=200,
- body=testEntities.read().decode('utf-8'))
+@app.route(route="get_eventhub_batch_triggered/{id}")
+@app.table_input(arg_name="testEntities",
+ connection="AzureWebJobsStorage",
+ table_name="EventHubBatchTest",
+ partition_key="{id}")
+def get_eventhub_batch_triggered(req: func.HttpRequest, testEntities):
+ return func.HttpResponse(status_code=200, body=testEntities)
# Retrieve the event data from storage blob and return it as Http response
diff --git a/tests/endtoend/eventhub_batch_functions/get_eventhub_batch_triggered/__init__.py b/tests/endtoend/eventhub_batch_functions/get_eventhub_batch_triggered/__init__.py
index feca352fb..8eccb90ee 100644
--- a/tests/endtoend/eventhub_batch_functions/get_eventhub_batch_triggered/__init__.py
+++ b/tests/endtoend/eventhub_batch_functions/get_eventhub_batch_triggered/__init__.py
@@ -4,6 +4,5 @@
# Retrieve the event data from storage blob and return it as Http response
-def main(req: func.HttpRequest, testEntities: func.InputStream):
- return func.HttpResponse(status_code=200,
- body=testEntities.read().decode('utf-8'))
+def main(req: func.HttpRequest, testEntities):
+ return func.HttpResponse(status_code=200, body=testEntities)
diff --git a/tests/endtoend/test_servicebus_functions.py b/tests/endtoend/test_servicebus_functions.py
index 48d71392e..aaacd76d6 100644
--- a/tests/endtoend/test_servicebus_functions.py
+++ b/tests/endtoend/test_servicebus_functions.py
@@ -2,16 +2,10 @@
# Licensed under the MIT License.
import json
import time
-from unittest import skipIf
-from azure_functions_worker.utils.common import is_envvar_true
from tests.utils import testutils
-from tests.utils.constants import DEDICATED_DOCKER_TEST, CONSUMPTION_DOCKER_TEST
-@skipIf(is_envvar_true(DEDICATED_DOCKER_TEST)
- or is_envvar_true(CONSUMPTION_DOCKER_TEST),
- "Skipping SB tests till docker image is updated with host 4.33")
class TestServiceBusFunctions(testutils.WebHostTestCase):
@classmethod
@@ -37,18 +31,16 @@ def test_servicebus_basic(self):
self.assertEqual(r.status_code, 200)
msg = r.json()
self.assertEqual(msg['body'], data)
- for attr in {
- 'message_id', 'body', 'content_type', 'delivery_count',
- 'expiration_time', 'label', 'partition_key', 'reply_to',
- 'reply_to_session_id', 'scheduled_enqueue_time',
- 'session_id', 'time_to_live', 'to', 'user_properties',
- 'application_properties', 'correlation_id',
- 'dead_letter_error_description', 'dead_letter_reason',
- 'dead_letter_source', 'enqueued_sequence_number',
- 'enqueued_time_utc', 'expires_at_utc', 'locked_until',
- 'lock_token', 'sequence_number', 'state', 'subject',
- 'transaction_partition_key'
- }:
+ for attr in {'message_id', 'body', 'content_type', 'delivery_count',
+ 'expiration_time', 'label', 'partition_key', 'reply_to',
+ 'reply_to_session_id', 'scheduled_enqueue_time',
+ 'session_id', 'time_to_live', 'to', 'user_properties',
+ 'application_properties', 'correlation_id',
+ 'dead_letter_error_description', 'dead_letter_reason',
+ 'dead_letter_source', 'enqueued_sequence_number',
+ 'enqueued_time_utc', 'expires_at_utc', 'locked_until',
+ 'lock_token', 'sequence_number', 'state', 'subject',
+ 'transaction_partition_key'}:
self.assertIn(attr, msg)
except (AssertionError, json.JSONDecodeError):
if try_no == max_retries - 1:
From 42d7a3dace025373b723232a44cb298de992b8ad Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Fri, 5 Apr 2024 19:31:37 -0700
Subject: [PATCH 022/101] fix
---
tests/unittests/test_dispatcher.py | 42 +++++++++++---------------
tests/unittests/test_http_functions.py | 9 ++++--
tests/utils/testutils.py | 3 +-
3 files changed, 25 insertions(+), 29 deletions(-)
diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py
index ba9670e24..37f23ea5f 100644
--- a/tests/unittests/test_dispatcher.py
+++ b/tests/unittests/test_dispatcher.py
@@ -9,14 +9,13 @@
from unittest.mock import patch
from azure_functions_worker import protos
-from azure_functions_worker.constants import (
- PYTHON_THREADPOOL_THREAD_COUNT,
- PYTHON_THREADPOOL_THREAD_COUNT_DEFAULT,
- PYTHON_THREADPOOL_THREAD_COUNT_MAX_37,
- PYTHON_THREADPOOL_THREAD_COUNT_MIN,
- PYTHON_ENABLE_INIT_INDEXING,
- METADATA_PROPERTIES_WORKER_INDEXED,
- PYTHON_ENABLE_DEBUG_LOGGING)
+from azure_functions_worker.constants import (PYTHON_THREADPOOL_THREAD_COUNT,
+ PYTHON_THREADPOOL_THREAD_COUNT_DEFAULT,
+ PYTHON_THREADPOOL_THREAD_COUNT_MAX_37,
+ PYTHON_THREADPOOL_THREAD_COUNT_MIN,
+ PYTHON_ENABLE_INIT_INDEXING,
+ METADATA_PROPERTIES_WORKER_INDEXED,
+ PYTHON_ENABLE_DEBUG_LOGGING)
from azure_functions_worker.dispatcher import Dispatcher
from azure_functions_worker.version import VERSION
from tests.utils import testutils
@@ -683,11 +682,9 @@ async def test_dispatcher_load_azfunc_in_init(self):
1
)
self.assertEqual(
- len([log for log in r.logs if
- log.message.startswith(
- "Received WorkerMetadataRequest from"
- "_handle__worker_init_request"
- )]),
+ len([log for log in r.logs if log.message.startswith(
+ "Received WorkerMetadataRequest from _handle__worker_init_request"
+ )]),
0
)
self.assertIn("azure.functions", sys.modules)
@@ -847,14 +844,11 @@ def test_functions_metadata_request_with_init_indexing_enabled(self):
self.assertEqual(init_response.worker_init_response.result.status,
protos.StatusResult.Success)
- metadata_response = \
- self.loop.run_until_complete(
- self.dispatcher._handle__functions_metadata_request(
- metadata_request))
+ metadata_response = self.loop.run_until_complete(
+ self.dispatcher._handle__functions_metadata_request(metadata_request))
- self.assertEqual(
- metadata_response.function_metadata_response.result.status,
- protos.StatusResult.Success)
+ self.assertEqual(metadata_response.function_metadata_response.result.status,
+ protos.StatusResult.Success)
self.assertIsNotNone(self.dispatcher._function_metadata_result)
self.assertIsNone(self.dispatcher._function_metadata_exception)
@@ -881,12 +875,10 @@ def test_functions_metadata_request_with_init_indexing_disabled(self):
self.assertIsNone(self.dispatcher._function_metadata_exception)
metadata_response = self.loop.run_until_complete(
- self.dispatcher._handle__functions_metadata_request(
- metadata_request))
+ self.dispatcher._handle__functions_metadata_request(metadata_request))
- self.assertEqual(
- metadata_response.function_metadata_response.result.status,
- protos.StatusResult.Success)
+ self.assertEqual(metadata_response.function_metadata_response.result.status,
+ protos.StatusResult.Success)
self.assertIsNotNone(self.dispatcher._function_metadata_result)
self.assertIsNone(self.dispatcher._function_metadata_exception)
diff --git a/tests/unittests/test_http_functions.py b/tests/unittests/test_http_functions.py
index 9819430d1..344ffdce8 100644
--- a/tests/unittests/test_http_functions.py
+++ b/tests/unittests/test_http_functions.py
@@ -10,7 +10,6 @@
from tests.utils import testutils
-
class TestHttpFunctions(testutils.WebHostTestCase):
@classmethod
@@ -461,4 +460,10 @@ def test_no_return(self):
def test_no_return_returns(self):
r = self.webhost.request('GET', 'no_return_returns')
- self.assertEqual(r.status_code, 200)
\ No newline at end of file
+ self.assertEqual(r.status_code, 200)
+
+class TestHttpFunctionsV2(TestHttpFunctions):
+ @classmethod
+ def get_script_dir(cls):
+ return testutils.UNIT_TESTS_FOLDER / 'http_functions' / \
+ 'http_v2_functions'
diff --git a/tests/utils/testutils.py b/tests/utils/testutils.py
index 86467f0c7..57946f1eb 100644
--- a/tests/utils/testutils.py
+++ b/tests/utils/testutils.py
@@ -249,8 +249,7 @@ def setUpClass(cls):
cls.host_stdout_logger.error(error_message)
raise RuntimeError(error_message)
except Exception as ex:
- cls.host_stdout_logger.error(
- f"WebHost is not started correctly. {ex}")
+ cls.host_stdout_logger.error(f"WebHost is not started correctly. {ex}")
cls.tearDownClass()
raise
From 21589e543e17fce28c63a84ccea1fa69082abb7f Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Fri, 5 Apr 2024 19:36:14 -0700
Subject: [PATCH 023/101] fix
---
.../function_app.py | 21 +++++++++----------
.../get_eventhub_batch_triggered/__init__.py | 4 ++--
tests/endtoend/test_servicebus_functions.py | 6 ++++++
tests/unittests/test_http_functions.py | 12 +----------
4 files changed, 19 insertions(+), 24 deletions(-)
diff --git a/tests/endtoend/eventhub_batch_functions/eventhub_batch_functions_stein/function_app.py b/tests/endtoend/eventhub_batch_functions/eventhub_batch_functions_stein/function_app.py
index bccc29d27..093d69228 100644
--- a/tests/endtoend/eventhub_batch_functions/eventhub_batch_functions_stein/function_app.py
+++ b/tests/endtoend/eventhub_batch_functions/eventhub_batch_functions_stein/function_app.py
@@ -18,10 +18,10 @@
connection="AzureWebJobsEventHubConnectionString",
data_type="string",
cardinality="many")
-@app.table_output(arg_name="$return",
- connection="AzureWebJobsStorage",
- table_name="EventHubBatchTest")
-def eventhub_multiple(events):
+@app.blob_output(arg_name="$return",
+ path="python-worker-tests/test-eventhub-batch-triggered.txt",
+ connection="AzureWebJobsStorage")
+def eventhub_multiple(events) -> str:
table_entries = []
for event in events:
json_entry = event.get_body()
@@ -46,13 +46,12 @@ def eventhub_output_batch(req: func.HttpRequest, out: func.Out[str]) -> str:
# Retrieve the event data from storage blob and return it as Http response
@app.function_name(name="get_eventhub_batch_triggered")
-@app.route(route="get_eventhub_batch_triggered/{id}")
-@app.table_input(arg_name="testEntities",
- connection="AzureWebJobsStorage",
- table_name="EventHubBatchTest",
- partition_key="{id}")
-def get_eventhub_batch_triggered(req: func.HttpRequest, testEntities):
- return func.HttpResponse(status_code=200, body=testEntities)
+@app.route(route="get_eventhub_batch_triggered")
+@app.blob_input(arg_name="testEntities",
+ path="python-worker-tests/test-eventhub-batch-triggered.txt",
+ connection="AzureWebJobsStorage")
+def get_eventhub_batch_triggered(req: func.HttpRequest, testEntities: func.InputStream):
+ return func.HttpResponse(status_code=200, body=testEntities.read().decode('utf-8'))
# Retrieve the event data from storage blob and return it as Http response
diff --git a/tests/endtoend/eventhub_batch_functions/get_eventhub_batch_triggered/__init__.py b/tests/endtoend/eventhub_batch_functions/get_eventhub_batch_triggered/__init__.py
index 8eccb90ee..153829b31 100644
--- a/tests/endtoend/eventhub_batch_functions/get_eventhub_batch_triggered/__init__.py
+++ b/tests/endtoend/eventhub_batch_functions/get_eventhub_batch_triggered/__init__.py
@@ -4,5 +4,5 @@
# Retrieve the event data from storage blob and return it as Http response
-def main(req: func.HttpRequest, testEntities):
- return func.HttpResponse(status_code=200, body=testEntities)
+def main(req: func.HttpRequest, testEntities: func.InputStream):
+ return func.HttpResponse(status_code=200, body=testEntities.read().decode('utf-8'))
diff --git a/tests/endtoend/test_servicebus_functions.py b/tests/endtoend/test_servicebus_functions.py
index aaacd76d6..34f51c5bc 100644
--- a/tests/endtoend/test_servicebus_functions.py
+++ b/tests/endtoend/test_servicebus_functions.py
@@ -2,10 +2,16 @@
# Licensed under the MIT License.
import json
import time
+from unittest import skipIf
+from azure_functions_worker.utils.common import is_envvar_true
from tests.utils import testutils
+from tests.utils.constants import DEDICATED_DOCKER_TEST, CONSUMPTION_DOCKER_TEST
+@skipIf(is_envvar_true(DEDICATED_DOCKER_TEST)
+ or is_envvar_true(CONSUMPTION_DOCKER_TEST),
+ "Skipping SB tests till docker image is updated with host 4.33")
class TestServiceBusFunctions(testutils.WebHostTestCase):
@classmethod
diff --git a/tests/unittests/test_http_functions.py b/tests/unittests/test_http_functions.py
index 344ffdce8..109694d71 100644
--- a/tests/unittests/test_http_functions.py
+++ b/tests/unittests/test_http_functions.py
@@ -10,6 +10,7 @@
from tests.utils import testutils
+
class TestHttpFunctions(testutils.WebHostTestCase):
@classmethod
@@ -108,11 +109,6 @@ def check_log_debug_logging(self, host_out: typing.List[str]):
self.assertIn('logging error', host_out)
self.assertNotIn('logging debug', host_out)
- def test_debug_with_user_logging(self):
- r = self.webhost.request('GET', 'debug_user_logging')
- self.assertEqual(r.status_code, 200)
- self.assertEqual(r.text, 'OK-user-debug')
-
def check_log_debug_with_user_logging(self, host_out: typing.List[str]):
self.assertIn('logging info', host_out)
self.assertIn('logging warning', host_out)
@@ -461,9 +457,3 @@ def test_no_return(self):
def test_no_return_returns(self):
r = self.webhost.request('GET', 'no_return_returns')
self.assertEqual(r.status_code, 200)
-
-class TestHttpFunctionsV2(TestHttpFunctions):
- @classmethod
- def get_script_dir(cls):
- return testutils.UNIT_TESTS_FOLDER / 'http_functions' / \
- 'http_v2_functions'
From 69ed92d271bb7d7f7fd041a455c582172be2ef79 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Fri, 5 Apr 2024 19:37:44 -0700
Subject: [PATCH 024/101] revert
---
azure_functions_worker/loader.py | 53 +++++++++++++++-----------------
1 file changed, 25 insertions(+), 28 deletions(-)
diff --git a/azure_functions_worker/loader.py b/azure_functions_worker/loader.py
index e90888e3c..938fde64c 100644
--- a/azure_functions_worker/loader.py
+++ b/azure_functions_worker/loader.py
@@ -122,34 +122,31 @@ def build_variable_interval_retry(retry, max_retry_count, retry_strategy):
def process_indexed_function(functions_registry: functions.Registry,
indexed_functions):
- try:
- fx_metadata_results = []
- for indexed_function in indexed_functions:
- function_info = functions_registry.add_indexed_function(
- function=indexed_function)
-
- binding_protos = build_binding_protos(indexed_function)
- retry_protos = build_retry_protos(indexed_function)
-
- function_metadata = protos.RpcFunctionMetadata(
- name=function_info.name,
- function_id=function_info.function_id,
- managed_dependency_enabled=False, # only enabled for PowerShell
- directory=function_info.directory,
- script_file=indexed_function.function_script_file,
- entry_point=function_info.name,
- is_proxy=False, # not supported in V4
- language=PYTHON_LANGUAGE_RUNTIME,
- bindings=binding_protos,
- raw_bindings=indexed_function.get_raw_bindings(),
- retry_options=retry_protos,
- properties={METADATA_PROPERTIES_WORKER_INDEXED: "True"})
-
- fx_metadata_results.append(function_metadata)
- return fx_metadata_results
- except Exception as e:
- logger.error(f'Error in process_indexed_function. {e}', exc_info=True)
- raise e
+ fx_metadata_results = []
+ for indexed_function in indexed_functions:
+ function_info = functions_registry.add_indexed_function(
+ function=indexed_function)
+
+ binding_protos = build_binding_protos(indexed_function)
+ retry_protos = build_retry_protos(indexed_function)
+
+ function_metadata = protos.RpcFunctionMetadata(
+ name=function_info.name,
+ function_id=function_info.function_id,
+ managed_dependency_enabled=False, # only enabled for PowerShell
+ directory=function_info.directory,
+ script_file=indexed_function.function_script_file,
+ entry_point=function_info.name,
+ is_proxy=False, # not supported in V4
+ language=PYTHON_LANGUAGE_RUNTIME,
+ bindings=binding_protos,
+ raw_bindings=indexed_function.get_raw_bindings(),
+ retry_options=retry_protos,
+ properties={METADATA_PROPERTIES_WORKER_INDEXED: "True"})
+
+ fx_metadata_results.append(function_metadata)
+
+ return fx_metadata_results
@attach_message_to_exception(
From 43114e78b46a224f5218992a8eca68772fcc2522 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Fri, 5 Apr 2024 20:43:18 -0700
Subject: [PATCH 025/101] fix
---
azure_functions_worker/dispatcher.py | 1 +
azure_functions_worker/functions.py | 1 +
2 files changed, 2 insertions(+)
diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py
index 992c6c639..8cd3d0d67 100644
--- a/azure_functions_worker/dispatcher.py
+++ b/azure_functions_worker/dispatcher.py
@@ -568,6 +568,7 @@ async def _handle__invocation_request(self, request):
http_v2_enabled = False
if sys.version_info.minor >= \
BASE_EXT_SUPPORTED_PY_MINOR_VERSION \
+ and fi.trigger_metadata is not None \
and fi.trigger_metadata.get('type') == HTTP_TRIGGER:
from azure.functions.extension.base import HttpV2FeatureChecker
http_v2_enabled = HttpV2FeatureChecker.http_v2_enabled()
diff --git a/azure_functions_worker/functions.py b/azure_functions_worker/functions.py
index ba2b8509e..c5f00e040 100644
--- a/azure_functions_worker/functions.py
+++ b/azure_functions_worker/functions.py
@@ -307,6 +307,7 @@ def add_func_to_registry_and_return_funcinfo(self, function,
None
)
+ trigger_metadata = None
if http_trigger_param_name is not None:
trigger_metadata = {
"type": HTTP_TRIGGER,
From 8923a442bff78dd9bc3f764d0adfd744f7cbc99c Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Fri, 5 Apr 2024 21:18:12 -0700
Subject: [PATCH 026/101] fix
---
setup.py | 3 +-
tests/unittests/test_http_v2.py | 70 ++++++++++++++++++++-------------
2 files changed, 44 insertions(+), 29 deletions(-)
diff --git a/setup.py b/setup.py
index 5308ae768..e82b5eb33 100644
--- a/setup.py
+++ b/setup.py
@@ -79,7 +79,8 @@
)
else:
INSTALL_REQUIRES.extend(
- ("protobuf~=4.22.0", "grpcio-tools~=1.54.2", "grpcio~=1.54.2", "azure-functions-extension-base")
+ ("protobuf~=4.22.0", "grpcio-tools~=1.54.2", "grpcio~=1.54.2",
+ "azure-functions-extension-base")
)
EXTRA_REQUIRES = {
diff --git a/tests/unittests/test_http_v2.py b/tests/unittests/test_http_v2.py
index 048f84095..e9b77f2f6 100644
--- a/tests/unittests/test_http_v2.py
+++ b/tests/unittests/test_http_v2.py
@@ -24,68 +24,82 @@ def setUp(self):
def tearDown(self) -> None:
http_coordinator._context_references.clear()
- def test_set_http_request_new_invocation(self):
+ async def test_set_http_request_new_invocation(self):
# Test setting a new HTTP request
http_coordinator.set_http_request(self.invoc_id, self.http_request)
context_ref = http_coordinator._context_references.get(self.invoc_id)
self.assertIsNotNone(context_ref)
self.assertEqual(context_ref.http_request, self.http_request)
- def test_set_http_request_existing_invocation(self):
+ async def test_set_http_request_existing_invocation(self):
# Test updating an existing HTTP request
new_http_request = MagicMock()
- http_coordinator.set_http_request(self.invoc_id, self.http_request)
http_coordinator.set_http_request(self.invoc_id, new_http_request)
context_ref = http_coordinator._context_references.get(self.invoc_id)
self.assertIsNotNone(context_ref)
self.assertEqual(context_ref.http_request, new_http_request)
- def test_set_http_response_context_ref_null(self):
+ async def test_set_http_response_context_ref_null(self):
with self.assertRaises(Exception) as cm:
http_coordinator.set_http_response(self.invoc_id,
self.http_response)
self.assertEqual(cm.exception.args[0],
"No context reference found for invocation %s")
- def test_set_http_response(self):
+ async def test_set_http_response(self):
http_coordinator.set_http_request(self.invoc_id, self.http_request)
http_coordinator.set_http_response(self.invoc_id, self.http_response)
context_ref = http_coordinator._context_references[self.invoc_id]
self.assertEqual(context_ref.http_response, self.http_response)
- def test_get_http_request_async_existing_invocation(self):
+ async def test_get_http_request_async_existing_invocation(self):
# Test retrieving an existing HTTP request
http_coordinator.set_http_request(self.invoc_id,
self.http_request)
- loop = asyncio.get_event_loop()
- retrieved_request = loop.run_until_complete(
- http_coordinator.get_http_request_async(self.invoc_id))
- self.assertEqual(retrieved_request, self.http_request)
+ loop = asyncio.new_event_loop()
+
+ try:
+ retrieved_request = loop.run_until_complete(
+ http_coordinator.get_http_request_async(self.invoc_id))
+ self.assertEqual(retrieved_request, self.http_request)
+ finally:
+ loop.close()
- def test_get_http_request_async_wait_for_request(self):
+ async def test_get_http_request_async_wait_for_request(self):
# Test waiting for an HTTP request to become available
async def set_request_after_delay():
await asyncio.sleep(1)
- http_coordinator.set_http_request(self.invoc_id,
- self.http_request)
-
- loop = asyncio.get_event_loop()
- loop.create_task(set_request_after_delay())
- retrieved_request = loop.run_until_complete(
- http_coordinator.get_http_request_async(self.invoc_id))
- self.assertEqual(retrieved_request, self.http_request)
-
- def test_get_http_request_async_wait_forever(self):
+ http_coordinator.set_http_request(self.invoc_id, self.http_request)
+
+ # Create a new event loop in the main thread
+ loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(loop)
+
+ # Run the test
+ try:
+ loop.run_until_complete(set_request_after_delay())
+ retrieved_request = loop.run_until_complete(
+ http_coordinator.get_http_request_async(self.invoc_id))
+ self.assertEqual(retrieved_request, self.http_request)
+ finally:
+ loop.close() # Close the event loop when done
+
+ async def test_get_http_request_async_wait_forever(self):
# Test handling error when invoc_id is not found
invalid_invoc_id = "invalid_invocation"
- loop = asyncio.get_event_loop()
- with self.assertRaises(asyncio.TimeoutError):
- loop.run_until_complete(
- asyncio.wait_for(
- http_coordinator.get_http_request_async(invalid_invoc_id),
- timeout=1
+ # Create a new event loop in the main thread
+ loop = asyncio.new_event_loop()
+ try:
+ with self.assertRaises(asyncio.TimeoutError):
+ loop.run_until_complete(
+ asyncio.wait_for(
+ http_coordinator.get_http_request_async(
+ invalid_invoc_id),
+ timeout=1
+ )
)
- )
+ finally:
+ loop.close()
async def test_await_http_response_async_valid_invocation(self):
invoc_id = "valid_invocation"
From d90a2f8cc0de8daa5a430067d6ad3153b77d809b Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Fri, 5 Apr 2024 22:25:30 -0700
Subject: [PATCH 027/101] fix
---
tests/endtoend/test_http_functions.py | 2 +-
tests/unittests/test_http_functions_v2.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/tests/endtoend/test_http_functions.py b/tests/endtoend/test_http_functions.py
index c12e91ab6..f3a8a6e07 100644
--- a/tests/endtoend/test_http_functions.py
+++ b/tests/endtoend/test_http_functions.py
@@ -224,7 +224,7 @@ def tearDownClass(cls):
super().tearDownClass()
-@unittest.skipIf(sys.version_info <= (3, 7), "Skipping tests if <= Python 3.7")
+@unittest.skipIf(sys.version_info.minor <= 7, "Skipping tests <= Python 3.7")
class TestHttpFunctionsV2FastApiWithInitIndexing(
TestHttpFunctionsWithInitIndexing):
@classmethod
diff --git a/tests/unittests/test_http_functions_v2.py b/tests/unittests/test_http_functions_v2.py
index 202dce14d..49f931f92 100644
--- a/tests/unittests/test_http_functions_v2.py
+++ b/tests/unittests/test_http_functions_v2.py
@@ -14,7 +14,7 @@
from tests.utils import testutils
-@unittest.skipIf(sys.version_info <= (3, 7), "Skipping tests if <= Python 3.7")
+@unittest.skipIf(sys.version_info.minor <= 7, "Skipping tests <= Python 3.7")
class TestHttpFunctionsV2FastApi(testutils.WebHostTestCase):
@classmethod
def setUpClass(cls):
From f44a28fc5aba39159c70b9409fbb88dd992395d3 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Fri, 5 Apr 2024 23:45:39 -0700
Subject: [PATCH 028/101] fix
---
azure_functions_worker/bindings/meta.py | 7 +-
azure_functions_worker/dispatcher.py | 147 ++++++++++--------------
azure_functions_worker/http_v2.py | 14 +++
3 files changed, 78 insertions(+), 90 deletions(-)
diff --git a/azure_functions_worker/bindings/meta.py b/azure_functions_worker/bindings/meta.py
index 097d00d94..7d79b0de4 100644
--- a/azure_functions_worker/bindings/meta.py
+++ b/azure_functions_worker/bindings/meta.py
@@ -19,7 +19,7 @@
def _check_http_input_type_annotation(bind_name: str, pytype: type) -> bool:
if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION:
- ext_base = sys.modules.get('azure.functions.extension.base')
+ import azure.functions.extension.base as ext_base
if ext_base is not None and \
ext_base.HttpV2FeatureChecker.http_v2_enabled():
return ext_base.RequestTrackerMeta.check_type(pytype)
@@ -30,9 +30,8 @@ def _check_http_input_type_annotation(bind_name: str, pytype: type) -> bool:
def _check_http_output_type_annotation(bind_name: str, pytype: type) -> bool:
if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION:
- ext_base = sys.modules.get('azure.functions.extension.base')
- if ext_base is not None and \
- ext_base.HttpV2FeatureChecker.http_v2_enabled():
+ import azure.functions.extension.base as ext_base
+ if ext_base.HttpV2FeatureChecker.http_v2_enabled():
return ext_base.ResponseTrackerMeta.check_type(pytype)
binding = get_binding(bind_name)
diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py
index 8cd3d0d67..1175349fe 100644
--- a/azure_functions_worker/dispatcher.py
+++ b/azure_functions_worker/dispatcher.py
@@ -20,7 +20,6 @@
from datetime import datetime
import grpc
-import socket
from . import bindings, constants, functions, loader, protos
from .bindings.shared_memory_data_transfer import SharedMemoryManager
from .constants import (HTTP_TRIGGER, PYTHON_ROLLBACK_CWD_PATH,
@@ -36,7 +35,7 @@
METADATA_PROPERTIES_WORKER_INDEXED,
BASE_EXT_SUPPORTED_PY_MINOR_VERSION)
from .extension import ExtensionManager
-from .http_v2 import http_coordinator
+from .http_v2 import http_coordinator, get_unused_tcp_port
from .logging import disable_console_logging, enable_console_logging
from .logging import (logger, error_logger, is_system_log_category,
CONSOLE_LOG_PREFIX, format_exception)
@@ -62,19 +61,6 @@ def current(mcls):
return disp
-def get_unused_tcp_port():
- # Create a TCP socket
- tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
- # Bind it to a free port provided by the OS
- tcp_socket.bind(("", 0))
- # Get the port number
- port = tcp_socket.getsockname()[1]
- # Close the socket
- tcp_socket.close()
- # Return the port number
- return port
-
-
class Dispatcher(metaclass=DispatcherMeta):
_GRPC_STOP_RESPONSE = object()
@@ -91,7 +77,6 @@ def __init__(self, loop: BaseEventLoop, host: str, port: int,
self._functions = functions.Registry()
self._shmem_mgr = SharedMemoryManager()
self._old_task_factory = None
- self.function_metadata_result = None
self._has_http_func = False
# Used to store metadata returns
@@ -285,80 +270,70 @@ async def _dispatch_grpc_request(self, request):
self._grpc_resp_queue.put_nowait(resp)
async def _handle__worker_init_request(self, request):
- try:
- logger.info('Received WorkerInitRequest, '
- 'python version %s, '
- 'worker version %s, '
- 'request ID %s. '
- 'App Settings state: %s. '
- 'To enable debug level logging, please refer to '
- 'https://aka.ms/python-enable-debug-logging',
- sys.version,
- VERSION,
- self.request_id,
- get_python_appsetting_state()
- )
-
- worker_init_request = request.worker_init_request
- host_capabilities = worker_init_request.capabilities
- if constants.FUNCTION_DATA_CACHE in host_capabilities:
- val = host_capabilities[constants.FUNCTION_DATA_CACHE]
- self._function_data_cache_enabled = val == _TRUE
-
- capabilities = {
- constants.RAW_HTTP_BODY_BYTES: _TRUE,
- constants.TYPED_DATA_COLLECTION: _TRUE,
- constants.RPC_HTTP_BODY_ONLY: _TRUE,
- constants.WORKER_STATUS: _TRUE,
- constants.RPC_HTTP_TRIGGER_METADATA_REMOVED: _TRUE,
- constants.SHARED_MEMORY_DATA_TRANSFER: _TRUE,
- }
-
- if DependencyManager.should_load_cx_dependencies():
- DependencyManager.prioritize_customer_dependencies()
-
- if DependencyManager.is_in_linux_consumption():
- import azure.functions # NoQA
-
- # loading bindings registry and saving results to a static
- # dictionary which will be later used in the invocation request
- bindings.load_binding_registry()
+ logger.info('Received WorkerInitRequest, '
+ 'python version %s, '
+ 'worker version %s, '
+ 'request ID %s. '
+ 'App Settings state: %s. '
+ 'To enable debug level logging, please refer to '
+ 'https://aka.ms/python-enable-debug-logging',
+ sys.version,
+ VERSION,
+ self.request_id,
+ get_python_appsetting_state()
+ )
+
+ worker_init_request = request.worker_init_request
+ host_capabilities = worker_init_request.capabilities
+ if constants.FUNCTION_DATA_CACHE in host_capabilities:
+ val = host_capabilities[constants.FUNCTION_DATA_CACHE]
+ self._function_data_cache_enabled = val == _TRUE
+
+ capabilities = {
+ constants.RAW_HTTP_BODY_BYTES: _TRUE,
+ constants.TYPED_DATA_COLLECTION: _TRUE,
+ constants.RPC_HTTP_BODY_ONLY: _TRUE,
+ constants.WORKER_STATUS: _TRUE,
+ constants.RPC_HTTP_TRIGGER_METADATA_REMOVED: _TRUE,
+ constants.SHARED_MEMORY_DATA_TRANSFER: _TRUE,
+ }
+
+ if DependencyManager.should_load_cx_dependencies():
+ DependencyManager.prioritize_customer_dependencies()
+
+ if DependencyManager.is_in_linux_consumption():
+ import azure.functions # NoQA
+
+ # loading bindings registry and saving results to a static
+ # dictionary which will be later used in the invocation request
+ bindings.load_binding_registry()
+
+ if is_envvar_true(PYTHON_ENABLE_INIT_INDEXING):
+ try:
+ self.load_function_metadata(
+ worker_init_request.function_app_directory,
+ caller_info="worker_init_request")
+ except Exception as ex:
+ self._function_metadata_exception = ex
- if is_envvar_true(PYTHON_ENABLE_INIT_INDEXING):
- try:
- self.load_function_metadata(
- worker_init_request.function_app_directory,
- caller_info="worker_init_request")
- except Exception as ex:
- self._function_metadata_exception = ex
+ if self._has_http_func:
+ from azure.functions.extension.base \
+ import HttpV2FeatureChecker
- if self._has_http_func:
- from azure.functions.extension.base \
- import HttpV2FeatureChecker
+ if HttpV2FeatureChecker.http_v2_enabled():
+ capabilities[constants.HTTP_URI] = \
+ await self._initialize_http_server()
- if HttpV2FeatureChecker.http_v2_enabled():
- capabilities[constants.HTTP_URI] = \
- await self._initialize_http_server()
+ return protos.StreamingMessage(
+ request_id=self.request_id,
+ worker_init_response=protos.WorkerInitResponse(
+ capabilities=capabilities,
+ worker_metadata=self.get_worker_metadata(),
+ result=protos.StatusResult(
+ status=protos.StatusResult.Success),
+ ),
+ )
- return protos.StreamingMessage(
- request_id=self.request_id,
- worker_init_response=protos.WorkerInitResponse(
- capabilities=capabilities,
- worker_metadata=self.get_worker_metadata(),
- result=protos.StatusResult(
- status=protos.StatusResult.Success),
- ),
- )
- except Exception as e:
- logger.error("Error handling WorkerInitRequest: %s", str(e))
- return protos.StreamingMessage(
- request_id=self.request_id,
- worker_init_response=protos.WorkerInitResponse(
- result=protos.StatusResult(
- status=protos.StatusResult.Failure,
- exception=self._serialize_exception(e))
- ),
- )
async def _handle__worker_status_request(self, request):
# Logging is not necessary in this request since the response is used
diff --git a/azure_functions_worker/http_v2.py b/azure_functions_worker/http_v2.py
index 8209ea80c..2ad7e4643 100644
--- a/azure_functions_worker/http_v2.py
+++ b/azure_functions_worker/http_v2.py
@@ -1,5 +1,6 @@
import abc
import asyncio
+import socket
from typing import Dict
@@ -151,4 +152,17 @@ def _pop_http_response(self, invoc_id):
raise Exception("No http response found for invocation %s", invoc_id)
+def get_unused_tcp_port():
+ # Create a TCP socket
+ tcp_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ # Bind it to a free port provided by the OS
+ tcp_socket.bind(("", 0))
+ # Get the port number
+ port = tcp_socket.getsockname()[1]
+ # Close the socket
+ tcp_socket.close()
+ # Return the port number
+ return port
+
+
http_coordinator = HttpCoordinator()
From b807e780c0ab946dc41a6262f225740eece4e4c0 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Fri, 5 Apr 2024 23:49:31 -0700
Subject: [PATCH 029/101] fix
---
azure_functions_worker/dispatcher.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py
index 1175349fe..905a45af0 100644
--- a/azure_functions_worker/dispatcher.py
+++ b/azure_functions_worker/dispatcher.py
@@ -316,7 +316,8 @@ async def _handle__worker_init_request(self, request):
except Exception as ex:
self._function_metadata_exception = ex
- if self._has_http_func:
+ if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION \
+ and self._has_http_func:
from azure.functions.extension.base \
import HttpV2FeatureChecker
@@ -334,7 +335,6 @@ async def _handle__worker_init_request(self, request):
),
)
-
async def _handle__worker_status_request(self, request):
# Logging is not necessary in this request since the response is used
# for host to judge scale decisions of out-of-proc languages.
From ffc7a1f0c469c6e8765b2981dda76cccab75b87f Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Sat, 6 Apr 2024 01:16:39 -0700
Subject: [PATCH 030/101] fix
---
azure_functions_worker/bindings/meta.py | 3 +--
tests/unittests/test_dispatcher.py | 10 ----------
2 files changed, 1 insertion(+), 12 deletions(-)
diff --git a/azure_functions_worker/bindings/meta.py b/azure_functions_worker/bindings/meta.py
index 7d79b0de4..778f85a33 100644
--- a/azure_functions_worker/bindings/meta.py
+++ b/azure_functions_worker/bindings/meta.py
@@ -20,8 +20,7 @@
def _check_http_input_type_annotation(bind_name: str, pytype: type) -> bool:
if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION:
import azure.functions.extension.base as ext_base
- if ext_base is not None and \
- ext_base.HttpV2FeatureChecker.http_v2_enabled():
+ if ext_base.HttpV2FeatureChecker.http_v2_enabled():
return ext_base.RequestTrackerMeta.check_type(pytype)
binding = get_binding(bind_name)
diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py
index 37f23ea5f..52117dc21 100644
--- a/tests/unittests/test_dispatcher.py
+++ b/tests/unittests/test_dispatcher.py
@@ -590,16 +590,6 @@ class TestDispatcherStein(testutils.AsyncTestCase):
def setUp(self):
self._ctrl = testutils.start_mockhost(
script_root=DISPATCHER_STEIN_FUNCTIONS_DIR)
- self._pre_env = dict(os.environ)
- self.mock_version_info = patch(
- 'azure_functions_worker.dispatcher.sys.version_info',
- SysVersionInfo(3, 9, 0, 'final', 0))
- self.mock_version_info.start()
-
- def tearDown(self):
- os.environ.clear()
- os.environ.update(self._pre_env)
- self.mock_version_info.stop()
async def test_dispatcher_functions_metadata_request(self):
"""Test if the functions metadata response will be sent correctly
From f53ddac1246a7cf6966fbdc4b12dd65281dce25a Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Sat, 6 Apr 2024 01:34:00 -0700
Subject: [PATCH 031/101] fix
---
tests/unittests/test_enable_debug_logging_functions.py | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/tests/unittests/test_enable_debug_logging_functions.py b/tests/unittests/test_enable_debug_logging_functions.py
index 120d54dfe..6f3739809 100644
--- a/tests/unittests/test_enable_debug_logging_functions.py
+++ b/tests/unittests/test_enable_debug_logging_functions.py
@@ -65,6 +65,7 @@ class TestDebugLoggingDisabledFunctions(testutils.WebHostTestCase):
"""
@classmethod
def setUpClass(cls):
+ cls._pre_env = dict(os.environ)
os_environ = os.environ.copy()
os_environ[PYTHON_ENABLE_DEBUG_LOGGING] = '0'
cls._patch_environ = patch.dict('os.environ', os_environ)
@@ -73,8 +74,9 @@ def setUpClass(cls):
@classmethod
def tearDownClass(cls):
- os.environ.pop(PYTHON_ENABLE_DEBUG_LOGGING)
super().tearDownClass()
+ os.environ.clear()
+ os.environ.update(cls._pre_env)
cls._patch_environ.stop()
@classmethod
From d7d537be2ad636590cb3fb53e76057b08d7f99f3 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Sat, 6 Apr 2024 01:45:02 -0700
Subject: [PATCH 032/101] fix
---
tests/unittests/test_http_functions_v2.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/tests/unittests/test_http_functions_v2.py b/tests/unittests/test_http_functions_v2.py
index 49f931f92..cf0e2a166 100644
--- a/tests/unittests/test_http_functions_v2.py
+++ b/tests/unittests/test_http_functions_v2.py
@@ -18,6 +18,7 @@
class TestHttpFunctionsV2FastApi(testutils.WebHostTestCase):
@classmethod
def setUpClass(cls):
+ cls._pre_env = dict(os.environ)
os_environ = os.environ.copy()
# Turn on feature flag
os_environ[PYTHON_ENABLE_INIT_INDEXING] = '1'
@@ -28,6 +29,8 @@ def setUpClass(cls):
@classmethod
def tearDownClass(cls):
+ os.environ.clear()
+ os.environ.update(cls._pre_env)
cls._patch_environ.stop()
super().tearDownClass()
From 9682363d8235e5af34be241176d87a08ac0c08e1 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Sat, 6 Apr 2024 12:39:19 -0700
Subject: [PATCH 033/101] tests
---
azure_functions_worker/http_v2.py | 5 --
tests/unittests/test_http_v2.py | 97 ++++++++++++++++++++++++++++++-
2 files changed, 96 insertions(+), 6 deletions(-)
diff --git a/azure_functions_worker/http_v2.py b/azure_functions_worker/http_v2.py
index 2ad7e4643..d04c198a3 100644
--- a/azure_functions_worker/http_v2.py
+++ b/azure_functions_worker/http_v2.py
@@ -16,7 +16,6 @@ def __init__(self, event_class, http_request=None, http_response=None,
self._http_trigger_param_name = http_trigger_param_name
self._http_request_available_event = event_class()
self._http_response_available_event = event_class()
- self._rpc_invocation_ready_event = event_class()
@property
def http_request(self):
@@ -76,10 +75,6 @@ def http_request_available_event(self):
def http_response_available_event(self):
return self._http_response_available_event
- @property
- def rpc_invocation_ready_event(self):
- return self._rpc_invocation_ready_event
-
class AsyncContextReference(BaseContextReference):
def __init__(self, http_request=None, http_response=None, function=None,
diff --git a/tests/unittests/test_http_v2.py b/tests/unittests/test_http_v2.py
index e9b77f2f6..4b9e91137 100644
--- a/tests/unittests/test_http_v2.py
+++ b/tests/unittests/test_http_v2.py
@@ -3,7 +3,8 @@
import unittest
from unittest.mock import MagicMock
-from azure_functions_worker.http_v2 import http_coordinator
+from azure_functions_worker.http_v2 import http_coordinator, \
+ BaseContextReference, AsyncContextReference, SingletonMeta
class MockHttpRequest:
@@ -141,3 +142,97 @@ async def test_await_http_response_async_response_not_set(self):
await http_coordinator.await_http_response_async(invoc_id)
self.assertEqual(str(context.exception),
f"No http response found for invocation {invoc_id}")
+
+@unittest.skipIf(sys.version_info <= (3, 7), "Skipping tests if <= Python 3.7")
+class TestAsyncContextReference(unittest.TestCase):
+
+ def test_init(self):
+ ref = AsyncContextReference()
+ self.assertIsInstance(ref, AsyncContextReference)
+ self.assertTrue(ref.is_async)
+
+ def test_http_request_property(self):
+ ref = AsyncContextReference()
+ ref.http_request = object()
+ self.assertIsNotNone(ref.http_request)
+
+ def test_http_response_property(self):
+ ref = AsyncContextReference()
+ ref.http_response = object()
+ self.assertIsNotNone(ref.http_response)
+
+ def test_function_property(self):
+ ref = AsyncContextReference()
+ ref.function = object()
+ self.assertIsNotNone(ref.function)
+
+ def test_fi_context_property(self):
+ ref = AsyncContextReference()
+ ref.fi_context = object()
+ self.assertIsNotNone(ref.fi_context)
+
+ def test_http_trigger_param_name_property(self):
+ ref = AsyncContextReference()
+ ref.http_trigger_param_name = object()
+ self.assertIsNotNone(ref.http_trigger_param_name)
+
+ def test_args_property(self):
+ ref = AsyncContextReference()
+ ref.args = object()
+ self.assertIsNotNone(ref.args)
+
+ def test_http_request_available_event_property(self):
+ ref = AsyncContextReference()
+ self.assertIsNotNone(ref.http_request_available_event)
+
+ def test_http_response_available_event_property(self):
+ ref = AsyncContextReference()
+ self.assertIsNotNone(ref.http_response_available_event)
+
+ def test_full_args(self):
+ ref = AsyncContextReference(http_request=object(),
+ http_response=object(),
+ function=object(),
+ fi_context=object(),
+ args=object())
+ self.assertIsNotNone(ref.http_request)
+ self.assertIsNotNone(ref.http_response)
+ self.assertIsNotNone(ref.function)
+ self.assertIsNotNone(ref.fi_context)
+ self.assertIsNotNone(ref.args)
+
+
+class TestSingletonMeta(unittest.TestCase):
+
+ def test_singleton_instance(self):
+ class TestClass(metaclass=SingletonMeta):
+ pass
+
+ obj1 = TestClass()
+ obj2 = TestClass()
+
+ self.assertIs(obj1, obj2)
+
+ def test_singleton_with_arguments(self):
+ class TestClass(metaclass=SingletonMeta):
+ def __init__(self, arg):
+ self.arg = arg
+
+ obj1 = TestClass(1)
+ obj2 = TestClass(2)
+
+ self.assertEqual(obj1.arg, 1)
+ self.assertEqual(obj2.arg,
+ 1) # Should still refer to the same instance
+
+ def test_singleton_with_kwargs(self):
+ class TestClass(metaclass=SingletonMeta):
+ def __init__(self, **kwargs):
+ self.kwargs = kwargs
+
+ obj1 = TestClass(a=1)
+ obj2 = TestClass(b=2)
+
+ self.assertEqual(obj1.kwargs, {'a': 1})
+ self.assertEqual(obj2.kwargs,
+ {'a': 1}) # Should still refer to the same instance
\ No newline at end of file
From a7505a0368d45bc89670b83d915e08227c1da8aa Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Sat, 6 Apr 2024 12:50:15 -0700
Subject: [PATCH 034/101] fix
---
tests/unittests/test_http_v2.py | 7 ++++---
1 file changed, 4 insertions(+), 3 deletions(-)
diff --git a/tests/unittests/test_http_v2.py b/tests/unittests/test_http_v2.py
index 4b9e91137..89cd25ced 100644
--- a/tests/unittests/test_http_v2.py
+++ b/tests/unittests/test_http_v2.py
@@ -4,7 +4,7 @@
from unittest.mock import MagicMock
from azure_functions_worker.http_v2 import http_coordinator, \
- BaseContextReference, AsyncContextReference, SingletonMeta
+ AsyncContextReference, SingletonMeta
class MockHttpRequest:
@@ -143,6 +143,7 @@ async def test_await_http_response_async_response_not_set(self):
self.assertEqual(str(context.exception),
f"No http response found for invocation {invoc_id}")
+
@unittest.skipIf(sys.version_info <= (3, 7), "Skipping tests if <= Python 3.7")
class TestAsyncContextReference(unittest.TestCase):
@@ -189,7 +190,7 @@ def test_http_response_available_event_property(self):
ref = AsyncContextReference()
self.assertIsNotNone(ref.http_response_available_event)
- def test_full_args(self):
+ def test_full_args(self):
ref = AsyncContextReference(http_request=object(),
http_response=object(),
function=object(),
@@ -235,4 +236,4 @@ def __init__(self, **kwargs):
self.assertEqual(obj1.kwargs, {'a': 1})
self.assertEqual(obj2.kwargs,
- {'a': 1}) # Should still refer to the same instance
\ No newline at end of file
+ {'a': 1}) # Should still refer to the same instance
From 5f3565e982598cc463b1247d0936ad289ad288af Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Sat, 6 Apr 2024 14:15:26 -0700
Subject: [PATCH 035/101] test
---
.../http_v2/fastapi/function_app.py | 10 ++++++++++
tests/unittests/test_dispatcher.py | 12 ++++++++++++
tests/unittests/test_http_v2.py | 4 ++++
3 files changed, 26 insertions(+)
create mode 100644 tests/unittests/dispatcher_functions/http_v2/fastapi/function_app.py
diff --git a/tests/unittests/dispatcher_functions/http_v2/fastapi/function_app.py b/tests/unittests/dispatcher_functions/http_v2/fastapi/function_app.py
new file mode 100644
index 000000000..f202890de
--- /dev/null
+++ b/tests/unittests/dispatcher_functions/http_v2/fastapi/function_app.py
@@ -0,0 +1,10 @@
+from azure.functions.extension.fastapi import Request, Response
+import azure.functions as func
+
+
+app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
+
+
+@app.route(route="http_trigger")
+def http_trigger(req: Request) -> Response:
+ return Response("ok")
diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py
index 52117dc21..6ed2ee306 100644
--- a/tests/unittests/test_dispatcher.py
+++ b/tests/unittests/test_dispatcher.py
@@ -27,6 +27,10 @@
DISPATCHER_STEIN_FUNCTIONS_DIR = testutils.UNIT_TESTS_FOLDER / \
'dispatcher_functions' / \
'dispatcher_functions_stein'
+DISPATCHER_HTTP_V2_FASTAPI_FUNCTIONS_DIR = testutils.UNIT_TESTS_FOLDER / \
+ 'dispatcher_functions' / \
+ 'http_v2' / \
+ 'fastapi'
FUNCTION_APP_DIRECTORY = UNIT_TESTS_ROOT / 'dispatcher_functions' / \
'dispatcher_functions_stein'
@@ -616,6 +620,14 @@ async def test_dispatcher_functions_metadata_request_with_retry(self):
protos.StatusResult.Success)
+@unittest.skipIf(sys.version_info.minor <= 7, "Skipping tests <= Python 3.7")
+class TestDispatcherHttpV2(TestDispatcherStein):
+
+ def setUp(self):
+ self._ctrl = testutils.start_mockhost(
+ script_root=DISPATCHER_HTTP_V2_FASTAPI_FUNCTIONS_DIR)
+
+
class TestDispatcherSteinLegacyFallback(testutils.AsyncTestCase):
def setUp(self):
diff --git a/tests/unittests/test_http_v2.py b/tests/unittests/test_http_v2.py
index 89cd25ced..f64291561 100644
--- a/tests/unittests/test_http_v2.py
+++ b/tests/unittests/test_http_v2.py
@@ -147,6 +147,10 @@ async def test_await_http_response_async_response_not_set(self):
@unittest.skipIf(sys.version_info <= (3, 7), "Skipping tests if <= Python 3.7")
class TestAsyncContextReference(unittest.TestCase):
+ def setUp(self):
+ loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(loop)
+
def test_init(self):
ref = AsyncContextReference()
self.assertIsInstance(ref, AsyncContextReference)
From b702aaf5a5143e8f8878da7594a27cd547af4df9 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Sat, 6 Apr 2024 16:04:06 -0700
Subject: [PATCH 036/101] tests
---
azure_functions_worker/bindings/meta.py | 9 ++++++---
tests/unittests/test_dispatcher.py | 22 ++++++++++++++++++++
tests/unittests/test_http_v2.py | 27 +++++++++++++++++++++++--
3 files changed, 53 insertions(+), 5 deletions(-)
diff --git a/azure_functions_worker/bindings/meta.py b/azure_functions_worker/bindings/meta.py
index 778f85a33..8c4627ed9 100644
--- a/azure_functions_worker/bindings/meta.py
+++ b/azure_functions_worker/bindings/meta.py
@@ -9,7 +9,8 @@
from . import datumdef
from . import generic
from .shared_memory_data_transfer import SharedMemoryManager
-from ..constants import BASE_EXT_SUPPORTED_PY_MINOR_VERSION
+from ..constants import BASE_EXT_SUPPORTED_PY_MINOR_VERSION, \
+ PYTHON_ENABLE_INIT_INDEXING
PB_TYPE = 'rpc_data'
PB_TYPE_DATA = 'data'
@@ -18,7 +19,8 @@
def _check_http_input_type_annotation(bind_name: str, pytype: type) -> bool:
- if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION:
+ if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION and \
+ is_envvar_true(PYTHON_ENABLE_INIT_INDEXING):
import azure.functions.extension.base as ext_base
if ext_base.HttpV2FeatureChecker.http_v2_enabled():
return ext_base.RequestTrackerMeta.check_type(pytype)
@@ -28,7 +30,8 @@ def _check_http_input_type_annotation(bind_name: str, pytype: type) -> bool:
def _check_http_output_type_annotation(bind_name: str, pytype: type) -> bool:
- if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION:
+ if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION and \
+ is_envvar_true(PYTHON_ENABLE_INIT_INDEXING):
import azure.functions.extension.base as ext_base
if ext_base.HttpV2FeatureChecker.http_v2_enabled():
return ext_base.ResponseTrackerMeta.check_type(pytype)
diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py
index 6ed2ee306..eb512af1b 100644
--- a/tests/unittests/test_dispatcher.py
+++ b/tests/unittests/test_dispatcher.py
@@ -627,6 +627,28 @@ def setUp(self):
self._ctrl = testutils.start_mockhost(
script_root=DISPATCHER_HTTP_V2_FASTAPI_FUNCTIONS_DIR)
+ async def test_dispatcher_index_without_init_should_fail(self):
+ """Test if the functions metadata response will be sent correctly
+ when a functions metadata request is received
+ """
+ async with self._ctrl as host:
+ await host.init_worker()
+ r = await host.get_functions_metadata()
+ self.assertIsInstance(r.response, protos.FunctionMetadataResponse)
+ self.assertFalse(r.response.use_default_metadata_indexing)
+ self.assertEqual(r.response.result.status,
+ protos.StatusResult.Failure)
+
+ @patch.dict(os.environ, {PYTHON_ENABLE_INIT_INDEXING: 'true'})
+ async def test_dispatcher_invoke_function(self):
+ async with self._ctrl as host:
+ await host.init_worker()
+ r = await host.get_functions_metadata()
+ self.assertIsInstance(r.response, protos.FunctionMetadataResponse)
+ self.assertFalse(r.response.use_default_metadata_indexing)
+ self.assertEqual(r.response.result.status,
+ protos.StatusResult.Success)
+
class TestDispatcherSteinLegacyFallback(testutils.AsyncTestCase):
diff --git a/tests/unittests/test_http_v2.py b/tests/unittests/test_http_v2.py
index f64291561..1df51b992 100644
--- a/tests/unittests/test_http_v2.py
+++ b/tests/unittests/test_http_v2.py
@@ -1,10 +1,11 @@
import asyncio
+import socket
import sys
import unittest
-from unittest.mock import MagicMock
+from unittest.mock import MagicMock, patch
from azure_functions_worker.http_v2 import http_coordinator, \
- AsyncContextReference, SingletonMeta
+ AsyncContextReference, SingletonMeta, get_unused_tcp_port
class MockHttpRequest:
@@ -241,3 +242,25 @@ def __init__(self, **kwargs):
self.assertEqual(obj1.kwargs, {'a': 1})
self.assertEqual(obj2.kwargs,
{'a': 1}) # Should still refer to the same instance
+
+
+class TestGetUnusedTCPPort(unittest.TestCase):
+
+ @patch('socket.socket')
+ def test_get_unused_tcp_port(self, mock_socket):
+ # Mock the socket object and its methods
+ mock_socket_instance = mock_socket.return_value
+ mock_socket_instance.getsockname.return_value = ('localhost', 12345)
+
+ # Call the function
+ port = get_unused_tcp_port()
+
+ # Assert that socket.socket was called with the correct arguments
+ mock_socket.assert_called_once_with(socket.AF_INET, socket.SOCK_STREAM)
+
+ # Assert that bind and close methods were called on the socket instance
+ mock_socket_instance.bind.assert_called_once_with(('', 0))
+ mock_socket_instance.close.assert_called_once()
+
+ # Assert that the returned port matches the expected value
+ self.assertEqual(port, 12345)
From 98df5dee2294939a114ee240f996f69ceeee5232 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Sat, 6 Apr 2024 16:08:11 -0700
Subject: [PATCH 037/101] fix
---
azure_functions_worker/bindings/meta.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/azure_functions_worker/bindings/meta.py b/azure_functions_worker/bindings/meta.py
index 8c4627ed9..b7daf5666 100644
--- a/azure_functions_worker/bindings/meta.py
+++ b/azure_functions_worker/bindings/meta.py
@@ -11,6 +11,7 @@
from .shared_memory_data_transfer import SharedMemoryManager
from ..constants import BASE_EXT_SUPPORTED_PY_MINOR_VERSION, \
PYTHON_ENABLE_INIT_INDEXING
+from ..utils.common import is_envvar_true
PB_TYPE = 'rpc_data'
PB_TYPE_DATA = 'data'
From 462fad4857ad819813c53a64b62bcfb1e68ba682 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Sat, 6 Apr 2024 16:30:03 -0700
Subject: [PATCH 038/101] fix
---
tests/unittests/test_dispatcher.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py
index eb512af1b..9b197454f 100644
--- a/tests/unittests/test_dispatcher.py
+++ b/tests/unittests/test_dispatcher.py
@@ -621,7 +621,7 @@ async def test_dispatcher_functions_metadata_request_with_retry(self):
@unittest.skipIf(sys.version_info.minor <= 7, "Skipping tests <= Python 3.7")
-class TestDispatcherHttpV2(TestDispatcherStein):
+class TestDispatcherHttpV2(testutils.AsyncTestCase):
def setUp(self):
self._ctrl = testutils.start_mockhost(
From c76c42cd68613c2431f190f298561e94b9b8b6f9 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Sat, 6 Apr 2024 16:42:16 -0700
Subject: [PATCH 039/101] fix
---
tests/unittests/test_dispatcher.py | 27 ++++++++++-----------------
1 file changed, 10 insertions(+), 17 deletions(-)
diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py
index 9b197454f..34dcfe2e9 100644
--- a/tests/unittests/test_dispatcher.py
+++ b/tests/unittests/test_dispatcher.py
@@ -631,23 +631,16 @@ async def test_dispatcher_index_without_init_should_fail(self):
"""Test if the functions metadata response will be sent correctly
when a functions metadata request is received
"""
- async with self._ctrl as host:
- await host.init_worker()
- r = await host.get_functions_metadata()
- self.assertIsInstance(r.response, protos.FunctionMetadataResponse)
- self.assertFalse(r.response.use_default_metadata_indexing)
- self.assertEqual(r.response.result.status,
- protos.StatusResult.Failure)
-
- @patch.dict(os.environ, {PYTHON_ENABLE_INIT_INDEXING: 'true'})
- async def test_dispatcher_invoke_function(self):
- async with self._ctrl as host:
- await host.init_worker()
- r = await host.get_functions_metadata()
- self.assertIsInstance(r.response, protos.FunctionMetadataResponse)
- self.assertFalse(r.response.use_default_metadata_indexing)
- self.assertEqual(r.response.result.status,
- protos.StatusResult.Success)
+ env = {PYTHON_ENABLE_INIT_INDEXING: "0"}
+ with patch.dict(os.environ, env):
+ async with self._ctrl as host:
+ await host.init_worker()
+ r = await host.get_functions_metadata()
+ self.assertIsInstance(r.response,
+ protos.FunctionMetadataResponse)
+ self.assertFalse(r.response.use_default_metadata_indexing)
+ self.assertEqual(r.response.result.status,
+ protos.StatusResult.Failure)
class TestDispatcherSteinLegacyFallback(testutils.AsyncTestCase):
From 90a7db637c10deadb8e43bcd1b89f4f5d05453bb Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Sat, 6 Apr 2024 16:49:09 -0700
Subject: [PATCH 040/101] move init server
---
azure_functions_worker/dispatcher.py | 48 ++--------------------------
azure_functions_worker/http_v2.py | 45 ++++++++++++++++++++++++++
2 files changed, 48 insertions(+), 45 deletions(-)
diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py
index 905a45af0..5af143eee 100644
--- a/azure_functions_worker/dispatcher.py
+++ b/azure_functions_worker/dispatcher.py
@@ -7,7 +7,6 @@
import asyncio
import concurrent.futures
-import importlib
import logging
import os
import platform
@@ -31,11 +30,10 @@
PYTHON_SCRIPT_FILE_NAME,
PYTHON_SCRIPT_FILE_NAME_DEFAULT,
PYTHON_LANGUAGE_RUNTIME, PYTHON_ENABLE_INIT_INDEXING,
- X_MS_INVOCATION_ID, LOCAL_HOST,
METADATA_PROPERTIES_WORKER_INDEXED,
BASE_EXT_SUPPORTED_PY_MINOR_VERSION)
from .extension import ExtensionManager
-from .http_v2 import http_coordinator, get_unused_tcp_port
+from .http_v2 import http_coordinator, initialize_http_server
from .logging import disable_console_logging, enable_console_logging
from .logging import (logger, error_logger, is_system_log_category,
CONSOLE_LOG_PREFIX, format_exception)
@@ -323,7 +321,7 @@ async def _handle__worker_init_request(self, request):
if HttpV2FeatureChecker.http_v2_enabled():
capabilities[constants.HTTP_URI] = \
- await self._initialize_http_server()
+ await initialize_http_server()
return protos.StreamingMessage(
request_id=self.request_id,
@@ -710,7 +708,7 @@ async def _handle__function_environment_reload_request(self, request):
if HttpV2FeatureChecker.http_v2_enabled():
capabilities[constants.HTTP_URI] = \
- await self._initialize_http_server()
+ await initialize_http_server()
# Change function app directory
if getattr(func_env_reload_request,
@@ -738,46 +736,6 @@ async def _handle__function_environment_reload_request(self, request):
request_id=self.request_id,
function_environment_reload_response=failure_response)
- async def _initialize_http_server(self):
- from azure.functions.extension.base \
- import ModuleTrackerMeta, RequestTrackerMeta
-
- web_extension_mod_name = ModuleTrackerMeta.get_module()
- extension_module = importlib.import_module(web_extension_mod_name)
- web_app_class = extension_module.WebApp
- web_server_class = extension_module.WebServer
-
- unused_port = get_unused_tcp_port()
-
- app = web_app_class()
- request_type = RequestTrackerMeta.get_request_type()
-
- @app.route
- async def catch_all(request: request_type): # type: ignore
- invoc_id = request.headers.get(X_MS_INVOCATION_ID)
- if invoc_id is None:
- raise ValueError(f"Header {X_MS_INVOCATION_ID} not found")
- logger.info('Received HTTP request for invocation %s', invoc_id)
- http_coordinator.set_http_request(invoc_id, request)
- http_resp = \
- await http_coordinator.await_http_response_async(invoc_id)
-
- logger.info('Sending HTTP response for invocation %s', invoc_id)
- # if http_resp is an python exception, raise it
- if isinstance(http_resp, Exception):
- raise http_resp
-
- return http_resp
-
- web_server = web_server_class(LOCAL_HOST, unused_port, app)
- web_server_run_task = web_server.serve()
-
- loop = asyncio.get_event_loop()
- loop.create_task(web_server_run_task)
- logger.info('HTTP server starting on %s:%s', LOCAL_HOST, unused_port)
-
- return f"http://{LOCAL_HOST}:{unused_port}"
-
def index_functions(self, function_path: str):
indexed_functions = loader.index_function_app(function_path)
logger.info(
diff --git a/azure_functions_worker/http_v2.py b/azure_functions_worker/http_v2.py
index d04c198a3..788b0e8f0 100644
--- a/azure_functions_worker/http_v2.py
+++ b/azure_functions_worker/http_v2.py
@@ -1,8 +1,12 @@
import abc
import asyncio
+import importlib
import socket
from typing import Dict
+from azure_functions_worker.constants import X_MS_INVOCATION_ID, LOCAL_HOST
+from azure_functions_worker.logging import logger
+
class BaseContextReference(abc.ABC):
def __init__(self, event_class, http_request=None, http_response=None,
@@ -160,4 +164,45 @@ def get_unused_tcp_port():
return port
+async def initialize_http_server():
+ from azure.functions.extension.base \
+ import ModuleTrackerMeta, RequestTrackerMeta
+
+ web_extension_mod_name = ModuleTrackerMeta.get_module()
+ extension_module = importlib.import_module(web_extension_mod_name)
+ web_app_class = extension_module.WebApp
+ web_server_class = extension_module.WebServer
+
+ unused_port = get_unused_tcp_port()
+
+ app = web_app_class()
+ request_type = RequestTrackerMeta.get_request_type()
+
+ @app.route
+ async def catch_all(request: request_type): # type: ignore
+ invoc_id = request.headers.get(X_MS_INVOCATION_ID)
+ if invoc_id is None:
+ raise ValueError(f"Header {X_MS_INVOCATION_ID} not found")
+ logger.info('Received HTTP request for invocation %s', invoc_id)
+ http_coordinator.set_http_request(invoc_id, request)
+ http_resp = \
+ await http_coordinator.await_http_response_async(invoc_id)
+
+ logger.info('Sending HTTP response for invocation %s', invoc_id)
+ # if http_resp is an python exception, raise it
+ if isinstance(http_resp, Exception):
+ raise http_resp
+
+ return http_resp
+
+ web_server = web_server_class(LOCAL_HOST, unused_port, app)
+ web_server_run_task = web_server.serve()
+
+ loop = asyncio.get_event_loop()
+ loop.create_task(web_server_run_task)
+ logger.info('HTTP server starting on %s:%s', LOCAL_HOST, unused_port)
+
+ return f"http://{LOCAL_HOST}:{unused_port}"
+
+
http_coordinator = HttpCoordinator()
From 988f9ca0e733b4d8b87c8a438ef78b894bd5b5eb Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Sat, 6 Apr 2024 17:14:14 -0700
Subject: [PATCH 041/101] fix
---
tests/unittests/test_dispatcher.py | 18 +++++++++++++++---
tests/utils/testutils.py | 7 +++++--
2 files changed, 20 insertions(+), 5 deletions(-)
diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py
index 34dcfe2e9..cfe7cfdca 100644
--- a/tests/unittests/test_dispatcher.py
+++ b/tests/unittests/test_dispatcher.py
@@ -624,13 +624,12 @@ async def test_dispatcher_functions_metadata_request_with_retry(self):
class TestDispatcherHttpV2(testutils.AsyncTestCase):
def setUp(self):
+ self.loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(self.loop)
self._ctrl = testutils.start_mockhost(
script_root=DISPATCHER_HTTP_V2_FASTAPI_FUNCTIONS_DIR)
async def test_dispatcher_index_without_init_should_fail(self):
- """Test if the functions metadata response will be sent correctly
- when a functions metadata request is received
- """
env = {PYTHON_ENABLE_INIT_INDEXING: "0"}
with patch.dict(os.environ, env):
async with self._ctrl as host:
@@ -642,6 +641,19 @@ async def test_dispatcher_index_without_init_should_fail(self):
self.assertEqual(r.response.result.status,
protos.StatusResult.Failure)
+ async def test_dispatcher_index_with_init_should_pass(self):
+ env = {PYTHON_ENABLE_INIT_INDEXING: "1"}
+ sys.path.append(str(DISPATCHER_HTTP_V2_FASTAPI_FUNCTIONS_DIR))
+ with patch.dict(os.environ, env):
+ async with self._ctrl as host:
+ await host.init_worker(include_func_app_dir=True)
+ r = await host.get_functions_metadata()
+ self.assertIsInstance(r.response,
+ protos.FunctionMetadataResponse)
+ self.assertFalse(r.response.use_default_metadata_indexing)
+ self.assertEqual(r.response.result.status,
+ protos.StatusResult.Success)
+
class TestDispatcherSteinLegacyFallback(testutils.AsyncTestCase):
diff --git a/tests/utils/testutils.py b/tests/utils/testutils.py
index 57946f1eb..ff76a5e21 100644
--- a/tests/utils/testutils.py
+++ b/tests/utils/testutils.py
@@ -529,11 +529,14 @@ def worker_id(self):
def request_id(self):
return self._request_id
- async def init_worker(self, host_version: str = '4.28.0'):
+ async def init_worker(self, host_version: str = '4.28.0', **kwargs):
+ include_func_app_dir = kwargs.get('include_func_app_dir', False)
r = await self.communicate(
protos.StreamingMessage(
worker_init_request=protos.WorkerInitRequest(
- host_version=host_version
+ host_version=host_version,
+ function_app_directory=
+ str(self._scripts_dir) if include_func_app_dir else None,
)
),
wait_for='worker_init_response'
From 67f679dceedb89844969ff5060a5227118a9a741 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Sat, 6 Apr 2024 17:26:51 -0700
Subject: [PATCH 042/101] test
---
tests/unittests/test_dispatcher.py | 14 ++++++++++++++
tests/utils/testutils.py | 4 ++--
2 files changed, 16 insertions(+), 2 deletions(-)
diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py
index cfe7cfdca..69dff67fa 100644
--- a/tests/unittests/test_dispatcher.py
+++ b/tests/unittests/test_dispatcher.py
@@ -654,6 +654,20 @@ async def test_dispatcher_index_with_init_should_pass(self):
self.assertEqual(r.response.result.status,
protos.StatusResult.Success)
+ async def test_dispatcher_environment_reload_with_init_should_pass(self):
+ async with self._ctrl as host:
+ # Reload environment variable on specialization
+ r = await host.reload_environment(
+ environment={PYTHON_ENABLE_INIT_INDEXING: "1"})
+ self.assertIsInstance(r.response,
+ protos.FunctionEnvironmentReloadResponse)
+ self.assertIsInstance(r.response.worker_metadata,
+ protos.WorkerMetadata)
+ self.assertEquals(r.response.worker_metadata.runtime_name,
+ "python")
+ self.assertEquals(r.response.worker_metadata.worker_version,
+ VERSION)
+
class TestDispatcherSteinLegacyFallback(testutils.AsyncTestCase):
diff --git a/tests/utils/testutils.py b/tests/utils/testutils.py
index ff76a5e21..bdc305291 100644
--- a/tests/utils/testutils.py
+++ b/tests/utils/testutils.py
@@ -535,8 +535,8 @@ async def init_worker(self, host_version: str = '4.28.0', **kwargs):
protos.StreamingMessage(
worker_init_request=protos.WorkerInitRequest(
host_version=host_version,
- function_app_directory=
- str(self._scripts_dir) if include_func_app_dir else None,
+ function_app_directory=str(
+ self._scripts_dir) if include_func_app_dir else None,
)
),
wait_for='worker_init_response'
From c41db1630bad12d8193adf477cc3678041a97d72 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Sun, 7 Apr 2024 00:10:23 -0700
Subject: [PATCH 043/101] update ppls
---
.github/workflows/ci_consumption_workflow.yml | 13 ++++++++
.github/workflows/ci_e2e_workflow.yml | 29 ++++++++++++-----
.github/workflows/ci_ut_workflow.yml | 31 +++++++++++++------
3 files changed, 56 insertions(+), 17 deletions(-)
diff --git a/.github/workflows/ci_consumption_workflow.yml b/.github/workflows/ci_consumption_workflow.yml
index 172e19f7e..d4580c7f1 100644
--- a/.github/workflows/ci_consumption_workflow.yml
+++ b/.github/workflows/ci_consumption_workflow.yml
@@ -29,13 +29,26 @@ jobs:
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
+ - name: Get Date
+ id: get-date
+ run: |
+ echo "todayDate=$(/bin/date -u "+%Y%m%d")" >> $GITHUB_ENV
+ shell: bash
+ - uses: actions/cache@v4
+ id: cache-pip
+ with:
+ path: ${{ env.pythonLocation }}
+ key: ${{ runner.os }}-pip-${{ hashFiles('setup.py') }}-${{ env.todayDate }}-${{ matrix.python-version }}
- name: Install dependencies
+ if: steps.cache-pip.outputs.cache-hit != 'true'
run: |
python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre
python -m pip install -U -e .[dev]
if [[ "${{ matrix.python-version }}" != "3.7" ]]; then
python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2]
fi
+ - name: Install worker
+ run: |
python setup.py build
- name: Running 3.7 Tests
if: matrix.python-version == 3.7
diff --git a/.github/workflows/ci_e2e_workflow.yml b/.github/workflows/ci_e2e_workflow.yml
index 6cbff4704..bbb20c88c 100644
--- a/.github/workflows/ci_e2e_workflow.yml
+++ b/.github/workflows/ci_e2e_workflow.yml
@@ -38,7 +38,27 @@ jobs:
uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0.x"
- - name: Install dependencies and the worker
+ - name: Get Date
+ id: get-date
+ run: |
+ echo "todayDate=$(/bin/date -u "+%Y%m%d")" >> $GITHUB_ENV
+ shell: bash
+ - uses: actions/cache@v4
+ id: cache-pip
+ with:
+ path: ${{ env.pythonLocation }}
+ key: ${{ runner.os }}-pip-${{ hashFiles('setup.py') }}-${{ env.todayDate }}-${{ matrix.python-version }}
+ - name: Install dependencies
+ if: steps.cache-pip.outputs.cache-hit != 'true'
+ run: |
+ python -m pip install --upgrade pip
+ python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre
+ python -m pip install -U -e .[dev]
+ # Conditionally install test dependencies for Python 3.8 and later
+ if [[ "${{ matrix.python-version }}" != "3.7" ]]; then
+ python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2]
+ fi
+ - name: Install worker
run: |
retry() {
local -r -i max_attempts="$1"; shift
@@ -57,13 +77,6 @@ jobs:
done
}
- python -m pip install --upgrade pip
- python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre
- python -m pip install -U -e .[dev]
- # Conditionally install test dependencies for Python 3.8 and later
- if [[ "${{ matrix.python-version }}" != "3.7" ]]; then
- python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2]
- fi
# Retry a couple times to avoid certificate issue
retry 5 python setup.py build
retry 5 python setup.py webhost --branch-name=dev
diff --git a/.github/workflows/ci_ut_workflow.yml b/.github/workflows/ci_ut_workflow.yml
index 1c3777e7a..cb92a14f9 100644
--- a/.github/workflows/ci_ut_workflow.yml
+++ b/.github/workflows/ci_ut_workflow.yml
@@ -37,7 +37,27 @@ jobs:
uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0.x"
- - name: Install dependencies and the worker
+ - name: Get Date
+ id: get-date
+ run: |
+ echo "todayDate=$(/bin/date -u "+%Y%m%d")" >> $GITHUB_ENV
+ shell: bash
+ - uses: actions/cache@v4
+ id: cache-pip
+ with:
+ path: ${{ env.pythonLocation }}
+ key: ${{ runner.os }}-pip-${{ hashFiles('setup.py') }}-${{ env.todayDate }}-${{ matrix.python-version }}
+ - name: Install dependencies
+ if: steps.cache-pip.outputs.cache-hit != 'true'
+ run: |
+ python -m pip install --upgrade pip
+ python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre
+ python -m pip install -U -e .[dev]
+ # Conditionally install test dependencies for Python 3.8 and later
+ if [[ "${{ matrix.python-version }}" != "3.7" ]]; then
+ python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2]
+ fi
+ - name: Install the worker
run: |
retry() {
local -r -i max_attempts="$1"; shift
@@ -55,14 +75,7 @@ jobs:
fi
done
}
-
- python -m pip install --upgrade pip
- python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre
- python -m pip install -U -e .[dev]
- # Conditionally install test dependencies for Python 3.8 and later
- if [[ "${{ matrix.python-version }}" != "3.7" ]]; then
- python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2]
- fi
+
# Retry a couple times to avoid certificate issue
retry 5 python setup.py build
retry 5 python setup.py webhost --branch-name=dev
From 3f0bfb620820a45af75f35afd65b2cfe3e678885 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Sun, 7 Apr 2024 00:30:21 -0700
Subject: [PATCH 044/101] fix test
---
tests/unittests/test_dispatcher.py | 12 ++++++------
tests/unittests/test_http_v2.py | 14 +++++++-------
2 files changed, 13 insertions(+), 13 deletions(-)
diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py
index 69dff67fa..52bc1e8ce 100644
--- a/tests/unittests/test_dispatcher.py
+++ b/tests/unittests/test_dispatcher.py
@@ -71,9 +71,9 @@ async def test_dispatcher_initialize_worker(self):
self.assertIsInstance(r.response, protos.WorkerInitResponse)
self.assertIsInstance(r.response.worker_metadata,
protos.WorkerMetadata)
- self.assertEquals(r.response.worker_metadata.runtime_name,
+ self.assertEqual(r.response.worker_metadata.runtime_name,
"python")
- self.assertEquals(r.response.worker_metadata.worker_version,
+ self.assertEqual(r.response.worker_metadata.worker_version,
VERSION)
async def test_dispatcher_environment_reload(self):
@@ -86,9 +86,9 @@ async def test_dispatcher_environment_reload(self):
protos.FunctionEnvironmentReloadResponse)
self.assertIsInstance(r.response.worker_metadata,
protos.WorkerMetadata)
- self.assertEquals(r.response.worker_metadata.runtime_name,
+ self.assertEqual(r.response.worker_metadata.runtime_name,
"python")
- self.assertEquals(r.response.worker_metadata.worker_version,
+ self.assertEqual(r.response.worker_metadata.worker_version,
VERSION)
async def test_dispatcher_initialize_worker_logging(self):
@@ -663,9 +663,9 @@ async def test_dispatcher_environment_reload_with_init_should_pass(self):
protos.FunctionEnvironmentReloadResponse)
self.assertIsInstance(r.response.worker_metadata,
protos.WorkerMetadata)
- self.assertEquals(r.response.worker_metadata.runtime_name,
+ self.assertEqual(r.response.worker_metadata.runtime_name,
"python")
- self.assertEquals(r.response.worker_metadata.worker_version,
+ self.assertEqual(r.response.worker_metadata.worker_version,
VERSION)
diff --git a/tests/unittests/test_http_v2.py b/tests/unittests/test_http_v2.py
index 1df51b992..e149e0144 100644
--- a/tests/unittests/test_http_v2.py
+++ b/tests/unittests/test_http_v2.py
@@ -26,14 +26,14 @@ def setUp(self):
def tearDown(self) -> None:
http_coordinator._context_references.clear()
- async def test_set_http_request_new_invocation(self):
+ def test_set_http_request_new_invocation(self):
# Test setting a new HTTP request
http_coordinator.set_http_request(self.invoc_id, self.http_request)
context_ref = http_coordinator._context_references.get(self.invoc_id)
self.assertIsNotNone(context_ref)
self.assertEqual(context_ref.http_request, self.http_request)
- async def test_set_http_request_existing_invocation(self):
+ def test_set_http_request_existing_invocation(self):
# Test updating an existing HTTP request
new_http_request = MagicMock()
http_coordinator.set_http_request(self.invoc_id, new_http_request)
@@ -41,20 +41,20 @@ async def test_set_http_request_existing_invocation(self):
self.assertIsNotNone(context_ref)
self.assertEqual(context_ref.http_request, new_http_request)
- async def test_set_http_response_context_ref_null(self):
+ def test_set_http_response_context_ref_null(self):
with self.assertRaises(Exception) as cm:
http_coordinator.set_http_response(self.invoc_id,
self.http_response)
self.assertEqual(cm.exception.args[0],
"No context reference found for invocation %s")
- async def test_set_http_response(self):
+ def test_set_http_response(self):
http_coordinator.set_http_request(self.invoc_id, self.http_request)
http_coordinator.set_http_response(self.invoc_id, self.http_response)
context_ref = http_coordinator._context_references[self.invoc_id]
self.assertEqual(context_ref.http_response, self.http_response)
- async def test_get_http_request_async_existing_invocation(self):
+ def test_get_http_request_async_existing_invocation(self):
# Test retrieving an existing HTTP request
http_coordinator.set_http_request(self.invoc_id,
self.http_request)
@@ -86,7 +86,7 @@ async def set_request_after_delay():
finally:
loop.close() # Close the event loop when done
- async def test_get_http_request_async_wait_forever(self):
+ def test_get_http_request_async_wait_forever(self):
# Test handling error when invoc_id is not found
invalid_invoc_id = "invalid_invocation"
# Create a new event loop in the main thread
@@ -103,7 +103,7 @@ async def test_get_http_request_async_wait_forever(self):
finally:
loop.close()
- async def test_await_http_response_async_valid_invocation(self):
+ def test_await_http_response_async_valid_invocation(self):
invoc_id = "valid_invocation"
expected_response = self.http_response
From 66f9edd52e17dcb77140f621ca72d8fc8b6078b3 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Sun, 7 Apr 2024 01:13:14 -0700
Subject: [PATCH 045/101] fix tests
---
azure_functions_worker/http_v2.py | 22 +++++-----
tests/unittests/test_dispatcher.py | 12 +++---
tests/unittests/test_http_v2.py | 69 +++++++++++++-----------------
3 files changed, 48 insertions(+), 55 deletions(-)
diff --git a/azure_functions_worker/http_v2.py b/azure_functions_worker/http_v2.py
index 788b0e8f0..e1030248b 100644
--- a/azure_functions_worker/http_v2.py
+++ b/azure_functions_worker/http_v2.py
@@ -110,8 +110,8 @@ def set_http_request(self, invoc_id, http_request):
def set_http_response(self, invoc_id, http_response):
if invoc_id not in self._context_references:
- raise Exception("No context reference found for invocation %s",
- invoc_id)
+ raise Exception("No context reference found for invocation "
+ f"{invoc_id}")
context_ref = self._context_references.get(invoc_id)
context_ref.http_response = http_response
@@ -126,8 +126,8 @@ async def get_http_request_async(self, invoc_id):
async def await_http_response_async(self, invoc_id):
if invoc_id not in self._context_references:
- raise Exception("No context reference found for invocation %s",
- invoc_id)
+ raise Exception("No context reference found for invocation "
+ f"{invoc_id}")
await asyncio.sleep(0)
await self._context_references.get(
invoc_id).http_response_available_event.wait()
@@ -140,7 +140,7 @@ def _pop_http_request(self, invoc_id):
context_ref.http_request = None
return request
- raise Exception("No http request found for invocation %s", invoc_id)
+ raise Exception(f"No http request found for invocation {invoc_id}")
def _pop_http_response(self, invoc_id):
context_ref = self._context_references.get(invoc_id)
@@ -148,7 +148,7 @@ def _pop_http_response(self, invoc_id):
if response is not None:
context_ref.http_response = None
return response
- raise Exception("No http response found for invocation %s", invoc_id)
+ raise Exception(f"No http response found for invocation {invoc_id}")
def get_unused_tcp_port():
@@ -183,12 +183,12 @@ async def catch_all(request: request_type): # type: ignore
invoc_id = request.headers.get(X_MS_INVOCATION_ID)
if invoc_id is None:
raise ValueError(f"Header {X_MS_INVOCATION_ID} not found")
- logger.info('Received HTTP request for invocation %s', invoc_id)
+ logger.info(f'Received HTTP request for invocation {invoc_id}')
http_coordinator.set_http_request(invoc_id, request)
http_resp = \
await http_coordinator.await_http_response_async(invoc_id)
- logger.info('Sending HTTP response for invocation %s', invoc_id)
+ logger.info(f'Sending HTTP response for invocation {invoc_id}')
# if http_resp is an python exception, raise it
if isinstance(http_resp, Exception):
raise http_resp
@@ -200,9 +200,11 @@ async def catch_all(request: request_type): # type: ignore
loop = asyncio.get_event_loop()
loop.create_task(web_server_run_task)
- logger.info('HTTP server starting on %s:%s', LOCAL_HOST, unused_port)
- return f"http://{LOCAL_HOST}:{unused_port}"
+ web_server_address = f"http://{LOCAL_HOST}:{unused_port}"
+ logger.info(f'HTTP server starting on {web_server_address}')
+
+ return web_server_address
http_coordinator = HttpCoordinator()
diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py
index 52bc1e8ce..2be8bf587 100644
--- a/tests/unittests/test_dispatcher.py
+++ b/tests/unittests/test_dispatcher.py
@@ -72,9 +72,9 @@ async def test_dispatcher_initialize_worker(self):
self.assertIsInstance(r.response.worker_metadata,
protos.WorkerMetadata)
self.assertEqual(r.response.worker_metadata.runtime_name,
- "python")
+ "python")
self.assertEqual(r.response.worker_metadata.worker_version,
- VERSION)
+ VERSION)
async def test_dispatcher_environment_reload(self):
"""Test function environment reload response
@@ -87,9 +87,9 @@ async def test_dispatcher_environment_reload(self):
self.assertIsInstance(r.response.worker_metadata,
protos.WorkerMetadata)
self.assertEqual(r.response.worker_metadata.runtime_name,
- "python")
+ "python")
self.assertEqual(r.response.worker_metadata.worker_version,
- VERSION)
+ VERSION)
async def test_dispatcher_initialize_worker_logging(self):
"""Test if the dispatcher's log can be flushed out during worker
@@ -664,9 +664,9 @@ async def test_dispatcher_environment_reload_with_init_should_pass(self):
self.assertIsInstance(r.response.worker_metadata,
protos.WorkerMetadata)
self.assertEqual(r.response.worker_metadata.runtime_name,
- "python")
+ "python")
self.assertEqual(r.response.worker_metadata.worker_version,
- VERSION)
+ VERSION)
class TestDispatcherSteinLegacyFallback(testutils.AsyncTestCase):
diff --git a/tests/unittests/test_http_v2.py b/tests/unittests/test_http_v2.py
index e149e0144..ab141120b 100644
--- a/tests/unittests/test_http_v2.py
+++ b/tests/unittests/test_http_v2.py
@@ -22,9 +22,12 @@ def setUp(self):
self.invoc_id = "test_invocation"
self.http_request = MockHttpRequest()
self.http_response = MockHttpResponse()
+ self.loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(self.loop)
def tearDown(self) -> None:
http_coordinator._context_references.clear()
+ self.loop.close()
def test_set_http_request_new_invocation(self):
# Test setting a new HTTP request
@@ -46,7 +49,8 @@ def test_set_http_response_context_ref_null(self):
http_coordinator.set_http_response(self.invoc_id,
self.http_response)
self.assertEqual(cm.exception.args[0],
- "No context reference found for invocation %s")
+ "No context reference found for invocation "
+ f"{self.invoc_id}")
def test_set_http_response(self):
http_coordinator.set_http_request(self.invoc_id, self.http_request)
@@ -58,14 +62,9 @@ def test_get_http_request_async_existing_invocation(self):
# Test retrieving an existing HTTP request
http_coordinator.set_http_request(self.invoc_id,
self.http_request)
- loop = asyncio.new_event_loop()
-
- try:
- retrieved_request = loop.run_until_complete(
- http_coordinator.get_http_request_async(self.invoc_id))
- self.assertEqual(retrieved_request, self.http_request)
- finally:
- loop.close()
+ retrieved_request = self.loop.run_until_complete(
+ http_coordinator.get_http_request_async(self.invoc_id))
+ self.assertEqual(retrieved_request, self.http_request)
async def test_get_http_request_async_wait_for_request(self):
# Test waiting for an HTTP request to become available
@@ -73,58 +72,49 @@ async def set_request_after_delay():
await asyncio.sleep(1)
http_coordinator.set_http_request(self.invoc_id, self.http_request)
- # Create a new event loop in the main thread
- loop = asyncio.new_event_loop()
- asyncio.set_event_loop(loop)
-
- # Run the test
- try:
- loop.run_until_complete(set_request_after_delay())
- retrieved_request = loop.run_until_complete(
- http_coordinator.get_http_request_async(self.invoc_id))
- self.assertEqual(retrieved_request, self.http_request)
- finally:
- loop.close() # Close the event loop when done
+ self.loop.run_until_complete(set_request_after_delay())
+ retrieved_request = self.loop.run_until_complete(
+ http_coordinator.get_http_request_async(self.invoc_id))
+ self.assertEqual(retrieved_request, self.http_request)
def test_get_http_request_async_wait_forever(self):
# Test handling error when invoc_id is not found
invalid_invoc_id = "invalid_invocation"
- # Create a new event loop in the main thread
- loop = asyncio.new_event_loop()
- try:
- with self.assertRaises(asyncio.TimeoutError):
- loop.run_until_complete(
- asyncio.wait_for(
- http_coordinator.get_http_request_async(
- invalid_invoc_id),
- timeout=1
- )
+
+ with self.assertRaises(asyncio.TimeoutError):
+ self.loop.run_until_complete(
+ asyncio.wait_for(
+ http_coordinator.get_http_request_async(
+ invalid_invoc_id),
+ timeout=1
)
- finally:
- loop.close()
+ )
def test_await_http_response_async_valid_invocation(self):
invoc_id = "valid_invocation"
expected_response = self.http_response
- context_ref = {}
- context_ref.http_response = expected_response
+ context_ref = AsyncContextReference(http_response=expected_response)
# Add the mock context reference to the coordinator
http_coordinator._context_references[invoc_id] = context_ref
+ http_coordinator.set_http_response(invoc_id, expected_response)
+
# Call the method and verify the returned response
- response = await http_coordinator.await_http_response_async(invoc_id)
+ response = self.loop.run_until_complete(
+ http_coordinator.await_http_response_async(invoc_id))
self.assertEqual(response, expected_response)
self.assertTrue(
http_coordinator._context_references.get(
invoc_id).http_response is None)
- async def test_await_http_response_async_invalid_invocation(self):
+ def test_await_http_response_async_invalid_invocation(self):
# Test handling error when invoc_id is not found
invalid_invoc_id = "invalid_invocation"
with self.assertRaises(Exception) as context:
- await http_coordinator.await_http_response_async(invalid_invoc_id)
+ self.loop.run_until_complete(
+ http_coordinator.await_http_response_async(invalid_invoc_id))
self.assertEqual(str(context.exception),
f"No context reference found for invocation "
f"{invalid_invoc_id}")
@@ -140,7 +130,8 @@ async def test_await_http_response_async_response_not_set(self):
# Call the method and verify that it raises an exception
with self.assertRaises(Exception) as context:
- await http_coordinator.await_http_response_async(invoc_id)
+ self.loop.run_until_complete(
+ http_coordinator.await_http_response_async(invoc_id))
self.assertEqual(str(context.exception),
f"No http response found for invocation {invoc_id}")
From ddc5f390bcb1980146aa12786e7a031b80b2ae55 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Sun, 7 Apr 2024 01:52:43 -0700
Subject: [PATCH 046/101] fix
---
tests/unittests/test_http_v2.py | 24 ++++++++----------------
1 file changed, 8 insertions(+), 16 deletions(-)
diff --git a/tests/unittests/test_http_v2.py b/tests/unittests/test_http_v2.py
index ab141120b..1ab7af5e7 100644
--- a/tests/unittests/test_http_v2.py
+++ b/tests/unittests/test_http_v2.py
@@ -66,17 +66,6 @@ def test_get_http_request_async_existing_invocation(self):
http_coordinator.get_http_request_async(self.invoc_id))
self.assertEqual(retrieved_request, self.http_request)
- async def test_get_http_request_async_wait_for_request(self):
- # Test waiting for an HTTP request to become available
- async def set_request_after_delay():
- await asyncio.sleep(1)
- http_coordinator.set_http_request(self.invoc_id, self.http_request)
-
- self.loop.run_until_complete(set_request_after_delay())
- retrieved_request = self.loop.run_until_complete(
- http_coordinator.get_http_request_async(self.invoc_id))
- self.assertEqual(retrieved_request, self.http_request)
-
def test_get_http_request_async_wait_forever(self):
# Test handling error when invoc_id is not found
invalid_invoc_id = "invalid_invocation"
@@ -119,15 +108,15 @@ def test_await_http_response_async_invalid_invocation(self):
f"No context reference found for invocation "
f"{invalid_invoc_id}")
- async def test_await_http_response_async_response_not_set(self):
+ def test_await_http_response_async_response_not_set(self):
invoc_id = "invocation_with_no_response"
# Set up a mock context reference without setting the response
- context_ref = {}
- context_ref.http_response = None
+ context_ref = AsyncContextReference()
# Add the mock context reference to the coordinator
http_coordinator._context_references[invoc_id] = context_ref
+ http_coordinator.set_http_response(invoc_id, None)
# Call the method and verify that it raises an exception
with self.assertRaises(Exception) as context:
self.loop.run_until_complete(
@@ -140,8 +129,11 @@ async def test_await_http_response_async_response_not_set(self):
class TestAsyncContextReference(unittest.TestCase):
def setUp(self):
- loop = asyncio.new_event_loop()
- asyncio.set_event_loop(loop)
+ self.loop = asyncio.new_event_loop()
+ asyncio.set_event_loop(self.loop)
+
+ def tearDown(self) -> None:
+ self.loop.close()
def test_init(self):
ref = AsyncContextReference()
From 5ce390aee30949e9f5ca3764e4b374931aefff30 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Sun, 7 Apr 2024 01:55:18 -0700
Subject: [PATCH 047/101] fix
---
tests/unittests/test_dispatcher.py | 3 +++
1 file changed, 3 insertions(+)
diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py
index 2be8bf587..113534a30 100644
--- a/tests/unittests/test_dispatcher.py
+++ b/tests/unittests/test_dispatcher.py
@@ -629,6 +629,9 @@ def setUp(self):
self._ctrl = testutils.start_mockhost(
script_root=DISPATCHER_HTTP_V2_FASTAPI_FUNCTIONS_DIR)
+ def tearDown(self):
+ self.loop.close()
+
async def test_dispatcher_index_without_init_should_fail(self):
env = {PYTHON_ENABLE_INIT_INDEXING: "0"}
with patch.dict(os.environ, env):
From bc9ca7f02436a52d958ac68c8510fbeaacaf2923 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Sun, 7 Apr 2024 01:57:33 -0700
Subject: [PATCH 048/101] fix
---
azure_functions_worker/bindings/meta.py | 6 ++----
1 file changed, 2 insertions(+), 4 deletions(-)
diff --git a/azure_functions_worker/bindings/meta.py b/azure_functions_worker/bindings/meta.py
index b7daf5666..14d7c132c 100644
--- a/azure_functions_worker/bindings/meta.py
+++ b/azure_functions_worker/bindings/meta.py
@@ -20,8 +20,7 @@
def _check_http_input_type_annotation(bind_name: str, pytype: type) -> bool:
- if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION and \
- is_envvar_true(PYTHON_ENABLE_INIT_INDEXING):
+ if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION:
import azure.functions.extension.base as ext_base
if ext_base.HttpV2FeatureChecker.http_v2_enabled():
return ext_base.RequestTrackerMeta.check_type(pytype)
@@ -31,8 +30,7 @@ def _check_http_input_type_annotation(bind_name: str, pytype: type) -> bool:
def _check_http_output_type_annotation(bind_name: str, pytype: type) -> bool:
- if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION and \
- is_envvar_true(PYTHON_ENABLE_INIT_INDEXING):
+ if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION:
import azure.functions.extension.base as ext_base
if ext_base.HttpV2FeatureChecker.http_v2_enabled():
return ext_base.ResponseTrackerMeta.check_type(pytype)
From 680ed45a3550c114cd763b0e97289af38b8b3e14 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Sun, 7 Apr 2024 02:03:57 -0700
Subject: [PATCH 049/101] Revert "fix"
This reverts commit bc9ca7f02436a52d958ac68c8510fbeaacaf2923.
---
azure_functions_worker/bindings/meta.py | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/azure_functions_worker/bindings/meta.py b/azure_functions_worker/bindings/meta.py
index 14d7c132c..b7daf5666 100644
--- a/azure_functions_worker/bindings/meta.py
+++ b/azure_functions_worker/bindings/meta.py
@@ -20,7 +20,8 @@
def _check_http_input_type_annotation(bind_name: str, pytype: type) -> bool:
- if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION:
+ if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION and \
+ is_envvar_true(PYTHON_ENABLE_INIT_INDEXING):
import azure.functions.extension.base as ext_base
if ext_base.HttpV2FeatureChecker.http_v2_enabled():
return ext_base.RequestTrackerMeta.check_type(pytype)
@@ -30,7 +31,8 @@ def _check_http_input_type_annotation(bind_name: str, pytype: type) -> bool:
def _check_http_output_type_annotation(bind_name: str, pytype: type) -> bool:
- if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION:
+ if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION and \
+ is_envvar_true(PYTHON_ENABLE_INIT_INDEXING):
import azure.functions.extension.base as ext_base
if ext_base.HttpV2FeatureChecker.http_v2_enabled():
return ext_base.ResponseTrackerMeta.check_type(pytype)
From 526765f74fa9f8ffe7fb894e93255d96c738ca94 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Sun, 7 Apr 2024 11:24:15 -0700
Subject: [PATCH 050/101] skip
---
tests/unittests/test_dispatcher.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py
index 113534a30..5d2da0050 100644
--- a/tests/unittests/test_dispatcher.py
+++ b/tests/unittests/test_dispatcher.py
@@ -620,7 +620,7 @@ async def test_dispatcher_functions_metadata_request_with_retry(self):
protos.StatusResult.Success)
-@unittest.skipIf(sys.version_info.minor <= 7, "Skipping tests <= Python 3.7")
+@unittest.skip
class TestDispatcherHttpV2(testutils.AsyncTestCase):
def setUp(self):
From 45217415ae380a128c18824ad617750a398531aa Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Sun, 7 Apr 2024 13:31:44 -0700
Subject: [PATCH 051/101] fix tests
---
azure_functions_worker/dispatcher.py | 4 +--
azure_functions_worker/http_v2.py | 2 +-
tests/unittests/test_dispatcher.py | 42 ++++++++++++++++++----------
3 files changed, 31 insertions(+), 17 deletions(-)
diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py
index 5af143eee..6899a7929 100644
--- a/azure_functions_worker/dispatcher.py
+++ b/azure_functions_worker/dispatcher.py
@@ -321,7 +321,7 @@ async def _handle__worker_init_request(self, request):
if HttpV2FeatureChecker.http_v2_enabled():
capabilities[constants.HTTP_URI] = \
- await initialize_http_server()
+ initialize_http_server()
return protos.StreamingMessage(
request_id=self.request_id,
@@ -708,7 +708,7 @@ async def _handle__function_environment_reload_request(self, request):
if HttpV2FeatureChecker.http_v2_enabled():
capabilities[constants.HTTP_URI] = \
- await initialize_http_server()
+ initialize_http_server()
# Change function app directory
if getattr(func_env_reload_request,
diff --git a/azure_functions_worker/http_v2.py b/azure_functions_worker/http_v2.py
index e1030248b..91758d0a2 100644
--- a/azure_functions_worker/http_v2.py
+++ b/azure_functions_worker/http_v2.py
@@ -164,7 +164,7 @@ def get_unused_tcp_port():
return port
-async def initialize_http_server():
+def initialize_http_server():
from azure.functions.extension.base \
import ModuleTrackerMeta, RequestTrackerMeta
diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py
index 5d2da0050..24484a0a5 100644
--- a/tests/unittests/test_dispatcher.py
+++ b/tests/unittests/test_dispatcher.py
@@ -21,6 +21,7 @@
from tests.utils import testutils
from tests.utils.testutils import UNIT_TESTS_ROOT
+
SysVersionInfo = col.namedtuple("VersionInfo", ["major", "minor", "micro",
"releaselevel", "serial"])
DISPATCHER_FUNCTIONS_DIR = testutils.UNIT_TESTS_FOLDER / 'dispatcher_functions'
@@ -620,8 +621,10 @@ async def test_dispatcher_functions_metadata_request_with_retry(self):
protos.StatusResult.Success)
-@unittest.skip
+@unittest.skipIf(sys.version_info.minor <= 7, "Skipping tests <= Python 3.7")
class TestDispatcherHttpV2(testutils.AsyncTestCase):
+ def return_mock_url(*args, **kwargs):
+ return 'http://1.0.0.0'
def setUp(self):
self.loop = asyncio.new_event_loop()
@@ -644,9 +647,13 @@ async def test_dispatcher_index_without_init_should_fail(self):
self.assertEqual(r.response.result.status,
protos.StatusResult.Failure)
- async def test_dispatcher_index_with_init_should_pass(self):
+ @patch('azure_functions_worker.dispatcher.initialize_http_server')
+ async def test_dispatcher_index_with_init_should_pass(
+ self, mock_initiate_http_server):
+
+ mock_initiate_http_server.side_effect = self.return_mock_url
env = {PYTHON_ENABLE_INIT_INDEXING: "1"}
- sys.path.append(str(DISPATCHER_HTTP_V2_FASTAPI_FUNCTIONS_DIR))
+
with patch.dict(os.environ, env):
async with self._ctrl as host:
await host.init_worker(include_func_app_dir=True)
@@ -657,11 +664,15 @@ async def test_dispatcher_index_with_init_should_pass(self):
self.assertEqual(r.response.result.status,
protos.StatusResult.Success)
- async def test_dispatcher_environment_reload_with_init_should_pass(self):
+ @patch('azure_functions_worker.dispatcher.initialize_http_server')
+ async def test_dispatcher_environment_reload_with_init_should_pass(
+ self, mock_initiate_http_server):
+ mock_initiate_http_server.side_effect = self.return_mock_url
async with self._ctrl as host:
# Reload environment variable on specialization
r = await host.reload_environment(
- environment={PYTHON_ENABLE_INIT_INDEXING: "1"})
+ environment={PYTHON_ENABLE_INIT_INDEXING: "1"},
+ function_project_path=str(host._scripts_dir))
self.assertIsInstance(r.response,
protos.FunctionEnvironmentReloadResponse)
self.assertIsInstance(r.response.worker_metadata,
@@ -729,7 +740,8 @@ async def test_dispatcher_load_azfunc_in_init(self):
)
self.assertEqual(
len([log for log in r.logs if log.message.startswith(
- "Received WorkerMetadataRequest from _handle__worker_init_request"
+ "Received WorkerMetadataRequest from "
+ "_handle__worker_init_request"
)]),
0
)
@@ -822,7 +834,6 @@ def tearDown(self):
@patch.dict(os.environ, {PYTHON_ENABLE_INIT_INDEXING: 'true'})
def test_worker_init_request_with_indexing_enabled(self):
-
request = protos.StreamingMessage(
worker_init_request=protos.WorkerInitRequest(
host_version="2.3.4",
@@ -891,10 +902,12 @@ def test_functions_metadata_request_with_init_indexing_enabled(self):
protos.StatusResult.Success)
metadata_response = self.loop.run_until_complete(
- self.dispatcher._handle__functions_metadata_request(metadata_request))
+ self.dispatcher._handle__functions_metadata_request(
+ metadata_request))
- self.assertEqual(metadata_response.function_metadata_response.result.status,
- protos.StatusResult.Success)
+ self.assertEqual(
+ metadata_response.function_metadata_response.result.status,
+ protos.StatusResult.Success)
self.assertIsNotNone(self.dispatcher._function_metadata_result)
self.assertIsNone(self.dispatcher._function_metadata_exception)
@@ -921,10 +934,12 @@ def test_functions_metadata_request_with_init_indexing_disabled(self):
self.assertIsNone(self.dispatcher._function_metadata_exception)
metadata_response = self.loop.run_until_complete(
- self.dispatcher._handle__functions_metadata_request(metadata_request))
+ self.dispatcher._handle__functions_metadata_request(
+ metadata_request))
- self.assertEqual(metadata_response.function_metadata_response.result.status,
- protos.StatusResult.Success)
+ self.assertEqual(
+ metadata_response.function_metadata_response.result.status,
+ protos.StatusResult.Success)
self.assertIsNotNone(self.dispatcher._function_metadata_result)
self.assertIsNone(self.dispatcher._function_metadata_exception)
@@ -933,7 +948,6 @@ def test_functions_metadata_request_with_init_indexing_disabled(self):
def test_functions_metadata_request_with_indexing_exception(
self,
mock_index_functions):
-
mock_index_functions.side_effect = Exception("Mocked Exception")
request = protos.StreamingMessage(
From f5fff784b4721417d1911ab5441d04e98f6ad4df Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Sun, 7 Apr 2024 13:41:36 -0700
Subject: [PATCH 052/101] style
---
azure_functions_worker/dispatcher.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py
index 6899a7929..161a74284 100644
--- a/azure_functions_worker/dispatcher.py
+++ b/azure_functions_worker/dispatcher.py
@@ -321,7 +321,7 @@ async def _handle__worker_init_request(self, request):
if HttpV2FeatureChecker.http_v2_enabled():
capabilities[constants.HTTP_URI] = \
- initialize_http_server()
+ initialize_http_server()
return protos.StreamingMessage(
request_id=self.request_id,
From 3e0075da484cc430b2590dc3dee4ea4ae33bb735 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Sun, 7 Apr 2024 13:47:27 -0700
Subject: [PATCH 053/101] test
---
tests/unittests/test_dispatcher.py | 56 +++++++++++++++---------------
1 file changed, 28 insertions(+), 28 deletions(-)
diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py
index 24484a0a5..8474bcf00 100644
--- a/tests/unittests/test_dispatcher.py
+++ b/tests/unittests/test_dispatcher.py
@@ -635,34 +635,34 @@ def setUp(self):
def tearDown(self):
self.loop.close()
- async def test_dispatcher_index_without_init_should_fail(self):
- env = {PYTHON_ENABLE_INIT_INDEXING: "0"}
- with patch.dict(os.environ, env):
- async with self._ctrl as host:
- await host.init_worker()
- r = await host.get_functions_metadata()
- self.assertIsInstance(r.response,
- protos.FunctionMetadataResponse)
- self.assertFalse(r.response.use_default_metadata_indexing)
- self.assertEqual(r.response.result.status,
- protos.StatusResult.Failure)
-
- @patch('azure_functions_worker.dispatcher.initialize_http_server')
- async def test_dispatcher_index_with_init_should_pass(
- self, mock_initiate_http_server):
-
- mock_initiate_http_server.side_effect = self.return_mock_url
- env = {PYTHON_ENABLE_INIT_INDEXING: "1"}
-
- with patch.dict(os.environ, env):
- async with self._ctrl as host:
- await host.init_worker(include_func_app_dir=True)
- r = await host.get_functions_metadata()
- self.assertIsInstance(r.response,
- protos.FunctionMetadataResponse)
- self.assertFalse(r.response.use_default_metadata_indexing)
- self.assertEqual(r.response.result.status,
- protos.StatusResult.Success)
+ # async def test_dispatcher_index_without_init_should_fail(self):
+ # env = {PYTHON_ENABLE_INIT_INDEXING: "0"}
+ # with patch.dict(os.environ, env):
+ # async with self._ctrl as host:
+ # await host.init_worker()
+ # r = await host.get_functions_metadata()
+ # self.assertIsInstance(r.response,
+ # protos.FunctionMetadataResponse)
+ # self.assertFalse(r.response.use_default_metadata_indexing)
+ # self.assertEqual(r.response.result.status,
+ # protos.StatusResult.Failure)
+
+ # @patch('azure_functions_worker.dispatcher.initialize_http_server')
+ # async def test_dispatcher_index_with_init_should_pass(
+ # self, mock_initiate_http_server):
+ #
+ # mock_initiate_http_server.side_effect = self.return_mock_url
+ # env = {PYTHON_ENABLE_INIT_INDEXING: "1"}
+ #
+ # with patch.dict(os.environ, env):
+ # async with self._ctrl as host:
+ # await host.init_worker(include_func_app_dir=True)
+ # r = await host.get_functions_metadata()
+ # self.assertIsInstance(r.response,
+ # protos.FunctionMetadataResponse)
+ # self.assertFalse(r.response.use_default_metadata_indexing)
+ # self.assertEqual(r.response.result.status,
+ # protos.StatusResult.Success)
@patch('azure_functions_worker.dispatcher.initialize_http_server')
async def test_dispatcher_environment_reload_with_init_should_pass(
From b96f74d443873ecd2fa4136ace8a28a0cbf45efc Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Sun, 7 Apr 2024 13:55:26 -0700
Subject: [PATCH 054/101] test
---
tests/unittests/test_dispatcher.py | 56 +++++++++++++++---------------
1 file changed, 28 insertions(+), 28 deletions(-)
diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py
index 8474bcf00..6958a0bd6 100644
--- a/tests/unittests/test_dispatcher.py
+++ b/tests/unittests/test_dispatcher.py
@@ -635,17 +635,17 @@ def setUp(self):
def tearDown(self):
self.loop.close()
- # async def test_dispatcher_index_without_init_should_fail(self):
- # env = {PYTHON_ENABLE_INIT_INDEXING: "0"}
- # with patch.dict(os.environ, env):
- # async with self._ctrl as host:
- # await host.init_worker()
- # r = await host.get_functions_metadata()
- # self.assertIsInstance(r.response,
- # protos.FunctionMetadataResponse)
- # self.assertFalse(r.response.use_default_metadata_indexing)
- # self.assertEqual(r.response.result.status,
- # protos.StatusResult.Failure)
+ async def test_dispatcher_index_without_init_should_fail(self):
+ env = {PYTHON_ENABLE_INIT_INDEXING: "0"}
+ with patch.dict(os.environ, env):
+ async with self._ctrl as host:
+ await host.init_worker()
+ r = await host.get_functions_metadata()
+ self.assertIsInstance(r.response,
+ protos.FunctionMetadataResponse)
+ self.assertFalse(r.response.use_default_metadata_indexing)
+ self.assertEqual(r.response.result.status,
+ protos.StatusResult.Failure)
# @patch('azure_functions_worker.dispatcher.initialize_http_server')
# async def test_dispatcher_index_with_init_should_pass(
@@ -664,23 +664,23 @@ def tearDown(self):
# self.assertEqual(r.response.result.status,
# protos.StatusResult.Success)
- @patch('azure_functions_worker.dispatcher.initialize_http_server')
- async def test_dispatcher_environment_reload_with_init_should_pass(
- self, mock_initiate_http_server):
- mock_initiate_http_server.side_effect = self.return_mock_url
- async with self._ctrl as host:
- # Reload environment variable on specialization
- r = await host.reload_environment(
- environment={PYTHON_ENABLE_INIT_INDEXING: "1"},
- function_project_path=str(host._scripts_dir))
- self.assertIsInstance(r.response,
- protos.FunctionEnvironmentReloadResponse)
- self.assertIsInstance(r.response.worker_metadata,
- protos.WorkerMetadata)
- self.assertEqual(r.response.worker_metadata.runtime_name,
- "python")
- self.assertEqual(r.response.worker_metadata.worker_version,
- VERSION)
+ # @patch('azure_functions_worker.dispatcher.initialize_http_server')
+ # async def test_dispatcher_environment_reload_with_init_should_pass(
+ # self, mock_initiate_http_server):
+ # mock_initiate_http_server.side_effect = self.return_mock_url
+ # async with self._ctrl as host:
+ # # Reload environment variable on specialization
+ # r = await host.reload_environment(
+ # environment={PYTHON_ENABLE_INIT_INDEXING: "1"},
+ # function_project_path=str(host._scripts_dir))
+ # self.assertIsInstance(r.response,
+ # protos.FunctionEnvironmentReloadResponse)
+ # self.assertIsInstance(r.response.worker_metadata,
+ # protos.WorkerMetadata)
+ # self.assertEqual(r.response.worker_metadata.runtime_name,
+ # "python")
+ # self.assertEqual(r.response.worker_metadata.worker_version,
+ # VERSION)
class TestDispatcherSteinLegacyFallback(testutils.AsyncTestCase):
From e68b41b1a9029cb6fb4339b9d9550a585416430c Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Sun, 7 Apr 2024 14:07:38 -0700
Subject: [PATCH 055/101] ff
---
tests/unittests/test_dispatcher.py | 67 +++++++++++++-----------------
1 file changed, 28 insertions(+), 39 deletions(-)
diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py
index 6958a0bd6..ef702cd5f 100644
--- a/tests/unittests/test_dispatcher.py
+++ b/tests/unittests/test_dispatcher.py
@@ -635,52 +635,41 @@ def setUp(self):
def tearDown(self):
self.loop.close()
- async def test_dispatcher_index_without_init_should_fail(self):
- env = {PYTHON_ENABLE_INIT_INDEXING: "0"}
+
+ @patch('azure_functions_worker.dispatcher.initialize_http_server')
+ async def test_dispatcher_index_with_init_should_pass(
+ self, mock_initiate_http_server):
+
+ mock_initiate_http_server.side_effect = self.return_mock_url
+ env = {PYTHON_ENABLE_INIT_INDEXING: "1"}
+
with patch.dict(os.environ, env):
async with self._ctrl as host:
- await host.init_worker()
+ await host.init_worker(include_func_app_dir=True)
r = await host.get_functions_metadata()
self.assertIsInstance(r.response,
protos.FunctionMetadataResponse)
self.assertFalse(r.response.use_default_metadata_indexing)
self.assertEqual(r.response.result.status,
- protos.StatusResult.Failure)
-
- # @patch('azure_functions_worker.dispatcher.initialize_http_server')
- # async def test_dispatcher_index_with_init_should_pass(
- # self, mock_initiate_http_server):
- #
- # mock_initiate_http_server.side_effect = self.return_mock_url
- # env = {PYTHON_ENABLE_INIT_INDEXING: "1"}
- #
- # with patch.dict(os.environ, env):
- # async with self._ctrl as host:
- # await host.init_worker(include_func_app_dir=True)
- # r = await host.get_functions_metadata()
- # self.assertIsInstance(r.response,
- # protos.FunctionMetadataResponse)
- # self.assertFalse(r.response.use_default_metadata_indexing)
- # self.assertEqual(r.response.result.status,
- # protos.StatusResult.Success)
-
- # @patch('azure_functions_worker.dispatcher.initialize_http_server')
- # async def test_dispatcher_environment_reload_with_init_should_pass(
- # self, mock_initiate_http_server):
- # mock_initiate_http_server.side_effect = self.return_mock_url
- # async with self._ctrl as host:
- # # Reload environment variable on specialization
- # r = await host.reload_environment(
- # environment={PYTHON_ENABLE_INIT_INDEXING: "1"},
- # function_project_path=str(host._scripts_dir))
- # self.assertIsInstance(r.response,
- # protos.FunctionEnvironmentReloadResponse)
- # self.assertIsInstance(r.response.worker_metadata,
- # protos.WorkerMetadata)
- # self.assertEqual(r.response.worker_metadata.runtime_name,
- # "python")
- # self.assertEqual(r.response.worker_metadata.worker_version,
- # VERSION)
+ protos.StatusResult.Success)
+
+ @patch('azure_functions_worker.dispatcher.initialize_http_server')
+ async def test_dispatcher_environment_reload_with_init_should_pass(
+ self, mock_initiate_http_server):
+ mock_initiate_http_server.side_effect = self.return_mock_url
+ async with self._ctrl as host:
+ # Reload environment variable on specialization
+ r = await host.reload_environment(
+ environment={PYTHON_ENABLE_INIT_INDEXING: "1"},
+ function_project_path=str(host._scripts_dir))
+ self.assertIsInstance(r.response,
+ protos.FunctionEnvironmentReloadResponse)
+ self.assertIsInstance(r.response.worker_metadata,
+ protos.WorkerMetadata)
+ self.assertEqual(r.response.worker_metadata.runtime_name,
+ "python")
+ self.assertEqual(r.response.worker_metadata.worker_version,
+ VERSION)
class TestDispatcherSteinLegacyFallback(testutils.AsyncTestCase):
From a58268840ee9dfc475a30bcf11e105a2308371e6 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Sun, 7 Apr 2024 14:45:17 -0700
Subject: [PATCH 056/101] test
---
tests/unittests/test_dispatcher.py | 5 -----
1 file changed, 5 deletions(-)
diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py
index ef702cd5f..be04066e1 100644
--- a/tests/unittests/test_dispatcher.py
+++ b/tests/unittests/test_dispatcher.py
@@ -627,14 +627,9 @@ def return_mock_url(*args, **kwargs):
return 'http://1.0.0.0'
def setUp(self):
- self.loop = asyncio.new_event_loop()
- asyncio.set_event_loop(self.loop)
self._ctrl = testutils.start_mockhost(
script_root=DISPATCHER_HTTP_V2_FASTAPI_FUNCTIONS_DIR)
- def tearDown(self):
- self.loop.close()
-
@patch('azure_functions_worker.dispatcher.initialize_http_server')
async def test_dispatcher_index_with_init_should_pass(
From d9bd32c36373bfcc7c641977e0b424c1be3b4e3f Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Sun, 7 Apr 2024 14:45:32 -0700
Subject: [PATCH 057/101] style
---
tests/unittests/test_dispatcher.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py
index be04066e1..91cbab44e 100644
--- a/tests/unittests/test_dispatcher.py
+++ b/tests/unittests/test_dispatcher.py
@@ -630,7 +630,6 @@ def setUp(self):
self._ctrl = testutils.start_mockhost(
script_root=DISPATCHER_HTTP_V2_FASTAPI_FUNCTIONS_DIR)
-
@patch('azure_functions_worker.dispatcher.initialize_http_server')
async def test_dispatcher_index_with_init_should_pass(
self, mock_initiate_http_server):
From d664f655db6e0f6955e107871ea04d8288874dcd Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Sun, 7 Apr 2024 15:28:26 -0700
Subject: [PATCH 058/101] skip
---
tests/unittests/test_dispatcher.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py
index 91cbab44e..5fa0b1719 100644
--- a/tests/unittests/test_dispatcher.py
+++ b/tests/unittests/test_dispatcher.py
@@ -621,7 +621,7 @@ async def test_dispatcher_functions_metadata_request_with_retry(self):
protos.StatusResult.Success)
-@unittest.skipIf(sys.version_info.minor <= 7, "Skipping tests <= Python 3.7")
+@unittest.skip
class TestDispatcherHttpV2(testutils.AsyncTestCase):
def return_mock_url(*args, **kwargs):
return 'http://1.0.0.0'
From 7bd566c125ae81d0a489a51b0c643d98f9e48956 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Sun, 7 Apr 2024 15:47:05 -0700
Subject: [PATCH 059/101] test
---
.github/workflows/ci_ut_workflow.yml | 3 ++-
tests/unittests/test_http_functions_v2.py | 3 ++-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/ci_ut_workflow.yml b/.github/workflows/ci_ut_workflow.yml
index cb92a14f9..b86dcd53e 100644
--- a/.github/workflows/ci_ut_workflow.yml
+++ b/.github/workflows/ci_ut_workflow.yml
@@ -86,7 +86,8 @@ jobs:
AzureWebJobsStorage: ${{ secrets.LinuxStorageConnectionString310 }} # needed for installing azure-functions-durable while running setup.py
ARCHIVE_WEBHOST_LOGS: ${{ github.event.inputs.archive_webhost_logging }}
run: |
- python -m pytest -q -n auto --dist loadfile --reruns 4 --instafail --cov=./azure_functions_worker --cov-report xml --cov-branch tests/unittests
+ python -m pytest -q -n auto --dist loadfile --reruns 4 --instafail --cov=./azure_functions_worker --cov-report xml --cov-branch -m "not serial" tests/unittests
+ python -m pytest -q --reruns 4 --instafail --cov=./azure_functions_worker --cov-report xml --cov-branch -m "serial" tests/unittests
- name: Codecov
uses: codecov/codecov-action@v3
with:
diff --git a/tests/unittests/test_http_functions_v2.py b/tests/unittests/test_http_functions_v2.py
index cf0e2a166..380d0b21c 100644
--- a/tests/unittests/test_http_functions_v2.py
+++ b/tests/unittests/test_http_functions_v2.py
@@ -12,7 +12,7 @@
from azure_functions_worker.constants import PYTHON_ENABLE_INIT_INDEXING
from tests.utils import testutils
-
+import pytest
@unittest.skipIf(sys.version_info.minor <= 7, "Skipping tests <= Python 3.7")
class TestHttpFunctionsV2FastApi(testutils.WebHostTestCase):
@@ -201,6 +201,7 @@ def test_accept_json(self):
self.assertEqual(r_json, {'a': 'abc', 'd': 42})
self.assertEqual(r.headers['content-type'], 'application/json')
+ @pytest.mark.serial
def test_unhandled_error(self):
r = self.webhost.request('GET', 'unhandled_error')
self.assertEqual(r.status_code, 500)
From 863f82bae99f24d6c6b5476cf8aa1fc08fb9e97b Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Sun, 7 Apr 2024 15:53:08 -0700
Subject: [PATCH 060/101] fix
---
pytest.ini | 3 +++
1 file changed, 3 insertions(+)
create mode 100644 pytest.ini
diff --git a/pytest.ini b/pytest.ini
new file mode 100644
index 000000000..56e02198d
--- /dev/null
+++ b/pytest.ini
@@ -0,0 +1,3 @@
+[pytest]
+markers =
+ serial: mark test as a serial test
From e220fc912e5a2e040c3f9e0b145636f5b8797b11 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Sun, 7 Apr 2024 15:54:16 -0700
Subject: [PATCH 061/101] fix
---
tests/unittests/test_http_functions_v2.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/tests/unittests/test_http_functions_v2.py b/tests/unittests/test_http_functions_v2.py
index 380d0b21c..ef4e9a073 100644
--- a/tests/unittests/test_http_functions_v2.py
+++ b/tests/unittests/test_http_functions_v2.py
@@ -14,6 +14,7 @@
from tests.utils import testutils
import pytest
+
@unittest.skipIf(sys.version_info.minor <= 7, "Skipping tests <= Python 3.7")
class TestHttpFunctionsV2FastApi(testutils.WebHostTestCase):
@classmethod
From 0ea9207ebadb26dfde13398d3465276e770f7166 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Sun, 7 Apr 2024 16:04:45 -0700
Subject: [PATCH 062/101] fix
---
.github/workflows/ci_ut_workflow.yml | 3 +--
tests/unittests/test_http_functions_v2.py | 4 +---
2 files changed, 2 insertions(+), 5 deletions(-)
diff --git a/.github/workflows/ci_ut_workflow.yml b/.github/workflows/ci_ut_workflow.yml
index b86dcd53e..cb92a14f9 100644
--- a/.github/workflows/ci_ut_workflow.yml
+++ b/.github/workflows/ci_ut_workflow.yml
@@ -86,8 +86,7 @@ jobs:
AzureWebJobsStorage: ${{ secrets.LinuxStorageConnectionString310 }} # needed for installing azure-functions-durable while running setup.py
ARCHIVE_WEBHOST_LOGS: ${{ github.event.inputs.archive_webhost_logging }}
run: |
- python -m pytest -q -n auto --dist loadfile --reruns 4 --instafail --cov=./azure_functions_worker --cov-report xml --cov-branch -m "not serial" tests/unittests
- python -m pytest -q --reruns 4 --instafail --cov=./azure_functions_worker --cov-report xml --cov-branch -m "serial" tests/unittests
+ python -m pytest -q -n auto --dist loadfile --reruns 4 --instafail --cov=./azure_functions_worker --cov-report xml --cov-branch tests/unittests
- name: Codecov
uses: codecov/codecov-action@v3
with:
diff --git a/tests/unittests/test_http_functions_v2.py b/tests/unittests/test_http_functions_v2.py
index ef4e9a073..be2eeb136 100644
--- a/tests/unittests/test_http_functions_v2.py
+++ b/tests/unittests/test_http_functions_v2.py
@@ -12,7 +12,6 @@
from azure_functions_worker.constants import PYTHON_ENABLE_INIT_INDEXING
from tests.utils import testutils
-import pytest
@unittest.skipIf(sys.version_info.minor <= 7, "Skipping tests <= Python 3.7")
@@ -202,7 +201,6 @@ def test_accept_json(self):
self.assertEqual(r_json, {'a': 'abc', 'd': 42})
self.assertEqual(r.headers['content-type'], 'application/json')
- @pytest.mark.serial
def test_unhandled_error(self):
r = self.webhost.request('GET', 'unhandled_error')
self.assertEqual(r.status_code, 500)
@@ -211,7 +209,7 @@ def test_unhandled_error(self):
def check_log_unhandled_error(self,
host_out: typing.List[str]):
- self.assertIn('Exception: ZeroDivisionError: division by zero',
+ self.assertIn('ZeroDivisionError: division by zero',
host_out)
def test_unhandled_urllib_error(self):
From 7e895282a943e1c79c6fc3d173cdc3f7327bde1a Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Sun, 7 Apr 2024 16:40:42 -0700
Subject: [PATCH 063/101] replay
---
.github/workflows/ci_ut_workflow.yml | 8 +++++++-
setup.py | 1 +
2 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/.github/workflows/ci_ut_workflow.yml b/.github/workflows/ci_ut_workflow.yml
index cb92a14f9..dd567466a 100644
--- a/.github/workflows/ci_ut_workflow.yml
+++ b/.github/workflows/ci_ut_workflow.yml
@@ -86,7 +86,7 @@ jobs:
AzureWebJobsStorage: ${{ secrets.LinuxStorageConnectionString310 }} # needed for installing azure-functions-durable while running setup.py
ARCHIVE_WEBHOST_LOGS: ${{ github.event.inputs.archive_webhost_logging }}
run: |
- python -m pytest -q -n auto --dist loadfile --reruns 4 --instafail --cov=./azure_functions_worker --cov-report xml --cov-branch tests/unittests
+ python -m pytest -q -n auto --replay-record-dir=build/tests/replay --dist loadfile --reruns 4 --instafail --cov=./azure_functions_worker --cov-report xml --cov-branch tests/unittests
- name: Codecov
uses: codecov/codecov-action@v3
with:
@@ -101,3 +101,9 @@ jobs:
name: Test WebHost Logs ${{ github.run_id }} ${{ matrix.python-version }}
path: logs/*.log
if-no-files-found: ignore
+ - name: Publish replays to Artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: Test Replays ${{ github.run_id }} ${{ matrix.python-version }}
+ path: build/tests/replay
+ if-no-files-found: ignore
diff --git a/setup.py b/setup.py
index e82b5eb33..bf60c456a 100644
--- a/setup.py
+++ b/setup.py
@@ -102,6 +102,7 @@
"pytest-randomly",
"pytest-instafail",
"pytest-rerunfailures",
+ "pytest-replay",
"ptvsd",
"python-dotenv",
"plotly",
From 43b346cb97b38c945a91e5d602fea257c1ef862d Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Sun, 7 Apr 2024 16:55:51 -0700
Subject: [PATCH 064/101] f
---
.github/workflows/ci_ut_workflow.yml | 1 +
1 file changed, 1 insertion(+)
diff --git a/.github/workflows/ci_ut_workflow.yml b/.github/workflows/ci_ut_workflow.yml
index dd567466a..0fe952d1a 100644
--- a/.github/workflows/ci_ut_workflow.yml
+++ b/.github/workflows/ci_ut_workflow.yml
@@ -102,6 +102,7 @@ jobs:
path: logs/*.log
if-no-files-found: ignore
- name: Publish replays to Artifact
+ if: failure()
uses: actions/upload-artifact@v4
with:
name: Test Replays ${{ github.run_id }} ${{ matrix.python-version }}
From 60553788f4ff658a5a11f4465b2132301149249b Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Sun, 7 Apr 2024 21:32:22 -0700
Subject: [PATCH 065/101] fix
---
tests/unittests/test_http_functions_v2.py | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/tests/unittests/test_http_functions_v2.py b/tests/unittests/test_http_functions_v2.py
index be2eeb136..45428a6b7 100644
--- a/tests/unittests/test_http_functions_v2.py
+++ b/tests/unittests/test_http_functions_v2.py
@@ -209,8 +209,13 @@ def test_unhandled_error(self):
def check_log_unhandled_error(self,
host_out: typing.List[str]):
- self.assertIn('ZeroDivisionError: division by zero',
- host_out)
+ error_substring = 'ZeroDivisionError: division by zero'
+ for item in host_out:
+ if error_substring in item:
+ break
+ else:
+ self.fail(
+ f"{error_substring}' not found in host log.")
def test_unhandled_urllib_error(self):
r = self.webhost.request(
From 0a8f6dfa05d1ac5bfd5fd2f7f6f625a969cb10b3 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Mon, 8 Apr 2024 00:59:16 -0700
Subject: [PATCH 066/101] codecov fix
---
.github/workflows/ci_ut_workflow.yml | 3 +-
tests/unittests/test_dispatcher.py | 45 ----------------------------
2 files changed, 2 insertions(+), 46 deletions(-)
diff --git a/.github/workflows/ci_ut_workflow.yml b/.github/workflows/ci_ut_workflow.yml
index 0fe952d1a..78e93eea4 100644
--- a/.github/workflows/ci_ut_workflow.yml
+++ b/.github/workflows/ci_ut_workflow.yml
@@ -88,12 +88,13 @@ jobs:
run: |
python -m pytest -q -n auto --replay-record-dir=build/tests/replay --dist loadfile --reruns 4 --instafail --cov=./azure_functions_worker --cov-report xml --cov-branch tests/unittests
- name: Codecov
- uses: codecov/codecov-action@v3
+ uses: codecov/codecov-action@v4
with:
file: ./coverage.xml # optional
flags: unittests # optional
name: codecov # optional
fail_ci_if_error: false # optional (default = false)
+ token: ${{ secrets.CODECOV_TOKEN }}
- name: Publish Logs to Artifact
if: failure()
uses: actions/upload-artifact@v4
diff --git a/tests/unittests/test_dispatcher.py b/tests/unittests/test_dispatcher.py
index 5fa0b1719..dc62acb8e 100644
--- a/tests/unittests/test_dispatcher.py
+++ b/tests/unittests/test_dispatcher.py
@@ -621,51 +621,6 @@ async def test_dispatcher_functions_metadata_request_with_retry(self):
protos.StatusResult.Success)
-@unittest.skip
-class TestDispatcherHttpV2(testutils.AsyncTestCase):
- def return_mock_url(*args, **kwargs):
- return 'http://1.0.0.0'
-
- def setUp(self):
- self._ctrl = testutils.start_mockhost(
- script_root=DISPATCHER_HTTP_V2_FASTAPI_FUNCTIONS_DIR)
-
- @patch('azure_functions_worker.dispatcher.initialize_http_server')
- async def test_dispatcher_index_with_init_should_pass(
- self, mock_initiate_http_server):
-
- mock_initiate_http_server.side_effect = self.return_mock_url
- env = {PYTHON_ENABLE_INIT_INDEXING: "1"}
-
- with patch.dict(os.environ, env):
- async with self._ctrl as host:
- await host.init_worker(include_func_app_dir=True)
- r = await host.get_functions_metadata()
- self.assertIsInstance(r.response,
- protos.FunctionMetadataResponse)
- self.assertFalse(r.response.use_default_metadata_indexing)
- self.assertEqual(r.response.result.status,
- protos.StatusResult.Success)
-
- @patch('azure_functions_worker.dispatcher.initialize_http_server')
- async def test_dispatcher_environment_reload_with_init_should_pass(
- self, mock_initiate_http_server):
- mock_initiate_http_server.side_effect = self.return_mock_url
- async with self._ctrl as host:
- # Reload environment variable on specialization
- r = await host.reload_environment(
- environment={PYTHON_ENABLE_INIT_INDEXING: "1"},
- function_project_path=str(host._scripts_dir))
- self.assertIsInstance(r.response,
- protos.FunctionEnvironmentReloadResponse)
- self.assertIsInstance(r.response.worker_metadata,
- protos.WorkerMetadata)
- self.assertEqual(r.response.worker_metadata.runtime_name,
- "python")
- self.assertEqual(r.response.worker_metadata.worker_version,
- VERSION)
-
-
class TestDispatcherSteinLegacyFallback(testutils.AsyncTestCase):
def setUp(self):
From 265c821c21c163e7ecf4eaceeb9c95ac04829dd4 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Mon, 8 Apr 2024 15:52:20 -0700
Subject: [PATCH 067/101] Mount base extension to fix consumption test failures
---
tests/utils/testutils_lc.py | 27 ++++++++++++++++++++++++++-
1 file changed, 26 insertions(+), 1 deletion(-)
diff --git a/tests/utils/testutils_lc.py b/tests/utils/testutils_lc.py
index 10c5079a9..19c81f15d 100644
--- a/tests/utils/testutils_lc.py
+++ b/tests/utils/testutils_lc.py
@@ -32,7 +32,8 @@
"/archive/refs/heads/dev.zip"
_FUNC_FILE_NAME = "azure-functions-python-library-dev"
_CUSTOM_IMAGE = "CUSTOM_IMAGE"
-
+_EXTENSION_BASE_ZIP = 'https://github.com/Azure/azure-functions-python-' \
+ 'extensions/archive/refs/heads/dev.zip'
class LinuxConsumptionWebHostController:
"""A controller for spawning mesh Docker container and apply multiple
@@ -151,6 +152,15 @@ def _download_azure_functions() -> str:
with ZipFile(BytesIO(zipresp.read())) as zfile:
zfile.extractall(tempfile.gettempdir())
+ @staticmethod
+ def _download_extensions() -> str:
+ folder = tempfile.gettempdir()
+ with urlopen(_EXTENSION_BASE_ZIP) as zipresp:
+ with ZipFile(BytesIO(zipresp.read())) as zfile:
+ zfile.extractall(folder)
+
+ return folder
+
def spawn_container(self,
image: str,
env: Dict[str, str] = {}) -> int:
@@ -163,11 +173,24 @@ def spawn_container(self,
# TODO: Mount library in docker container
# self._download_azure_functions()
+ # Download python extension base package
+ ext_folder = self._download_extensions()
+
container_worker_path = (
f"/azure-functions-host/workers/python/{self._py_version}/"
"LINUX/X64/azure_functions_worker"
)
+ base_ext_container_path = (
+ f"/azure-functions-host/workers/python/{self._py_version}/"
+ "LINUX/X64/azure/functions/extension/base"
+ )
+
+ base_ext_local_path = (
+ f'{ext_folder}\\azure-functions-python'
+ f'-extensions-dev\\azure-functions-extension-base'
+ f'\\azure\\functions\\extension\\base'
+ )
run_cmd = []
run_cmd.extend([self._docker_cmd, "run", "-p", "0:80", "-d"])
run_cmd.extend(["--name", self._uuid, "--privileged"])
@@ -177,6 +200,8 @@ def spawn_container(self,
run_cmd.extend(["-e", f"CONTAINER_ENCRYPTION_KEY={_DUMMY_CONT_KEY}"])
run_cmd.extend(["-e", "WEBSITE_PLACEHOLDER_MODE=1"])
run_cmd.extend(["-v", f'{worker_path}:{container_worker_path}'])
+ run_cmd.extend(["-v",
+ f'{base_ext_local_path}:{base_ext_container_path}'])
for key, value in env.items():
run_cmd.extend(["-e", f"{key}={value}"])
From 68c2d901f4d94a8f78430e467d5d3ba534d0e46d Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Mon, 8 Apr 2024 15:59:45 -0700
Subject: [PATCH 068/101] style
---
tests/utils/testutils_lc.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/tests/utils/testutils_lc.py b/tests/utils/testutils_lc.py
index 19c81f15d..84311a6c1 100644
--- a/tests/utils/testutils_lc.py
+++ b/tests/utils/testutils_lc.py
@@ -35,6 +35,7 @@
_EXTENSION_BASE_ZIP = 'https://github.com/Azure/azure-functions-python-' \
'extensions/archive/refs/heads/dev.zip'
+
class LinuxConsumptionWebHostController:
"""A controller for spawning mesh Docker container and apply multiple
test cases on it.
From 23bd8ab28a47054a62d729c2b1eb1259fdf477f4 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Mon, 8 Apr 2024 16:49:02 -0700
Subject: [PATCH 069/101] revert
---
pytest.ini | 3 ---
setup.py | 1 -
tests/utils/testutils.py | 7 ++-----
3 files changed, 2 insertions(+), 9 deletions(-)
delete mode 100644 pytest.ini
diff --git a/pytest.ini b/pytest.ini
deleted file mode 100644
index 56e02198d..000000000
--- a/pytest.ini
+++ /dev/null
@@ -1,3 +0,0 @@
-[pytest]
-markers =
- serial: mark test as a serial test
diff --git a/setup.py b/setup.py
index bf60c456a..e82b5eb33 100644
--- a/setup.py
+++ b/setup.py
@@ -102,7 +102,6 @@
"pytest-randomly",
"pytest-instafail",
"pytest-rerunfailures",
- "pytest-replay",
"ptvsd",
"python-dotenv",
"plotly",
diff --git a/tests/utils/testutils.py b/tests/utils/testutils.py
index bdc305291..57946f1eb 100644
--- a/tests/utils/testutils.py
+++ b/tests/utils/testutils.py
@@ -529,14 +529,11 @@ def worker_id(self):
def request_id(self):
return self._request_id
- async def init_worker(self, host_version: str = '4.28.0', **kwargs):
- include_func_app_dir = kwargs.get('include_func_app_dir', False)
+ async def init_worker(self, host_version: str = '4.28.0'):
r = await self.communicate(
protos.StreamingMessage(
worker_init_request=protos.WorkerInitRequest(
- host_version=host_version,
- function_app_directory=str(
- self._scripts_dir) if include_func_app_dir else None,
+ host_version=host_version
)
),
wait_for='worker_init_response'
From aac033d4280d7e1ae8c2a0944eadd32b1b93401d Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Mon, 8 Apr 2024 16:53:55 -0700
Subject: [PATCH 070/101] revert
---
.github/workflows/ci_ut_workflow.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/ci_ut_workflow.yml b/.github/workflows/ci_ut_workflow.yml
index 78e93eea4..8e95a0a4e 100644
--- a/.github/workflows/ci_ut_workflow.yml
+++ b/.github/workflows/ci_ut_workflow.yml
@@ -86,7 +86,7 @@ jobs:
AzureWebJobsStorage: ${{ secrets.LinuxStorageConnectionString310 }} # needed for installing azure-functions-durable while running setup.py
ARCHIVE_WEBHOST_LOGS: ${{ github.event.inputs.archive_webhost_logging }}
run: |
- python -m pytest -q -n auto --replay-record-dir=build/tests/replay --dist loadfile --reruns 4 --instafail --cov=./azure_functions_worker --cov-report xml --cov-branch tests/unittests
+ python -m pytest -q -n auto --dist loadfile --reruns 4 --instafail --cov=./azure_functions_worker --cov-report xml --cov-branch tests/unittests
- name: Codecov
uses: codecov/codecov-action@v4
with:
From 259ec03f632ab0461de3f25c9d4aec6f3301b833 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Tue, 9 Apr 2024 09:33:31 -0700
Subject: [PATCH 071/101] revert ppls
---
.github/workflows/ci_consumption_workflow.yml | 1 +
.github/workflows/ci_e2e_workflow.yml | 1 +
.github/workflows/ci_ut_workflow.yml | 1 +
.github/workflows/linter.yml | 7 ++-----
4 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/.github/workflows/ci_consumption_workflow.yml b/.github/workflows/ci_consumption_workflow.yml
index d4580c7f1..34ef6eb51 100644
--- a/.github/workflows/ci_consumption_workflow.yml
+++ b/.github/workflows/ci_consumption_workflow.yml
@@ -12,6 +12,7 @@ on:
push:
branches: [ dev, main, release/* ]
pull_request:
+ branches: [ dev, main, release/* ]
jobs:
build:
diff --git a/.github/workflows/ci_e2e_workflow.yml b/.github/workflows/ci_e2e_workflow.yml
index bbb20c88c..55862f50d 100644
--- a/.github/workflows/ci_e2e_workflow.yml
+++ b/.github/workflows/ci_e2e_workflow.yml
@@ -13,6 +13,7 @@ on:
push:
branches: [dev, main, release/*]
pull_request:
+ branches: [ dev, main, release/* ]
schedule:
# Monday to Thursday 3 AM CST build
# * is a special character in YAML so you have to quote this string
diff --git a/.github/workflows/ci_ut_workflow.yml b/.github/workflows/ci_ut_workflow.yml
index 8e95a0a4e..a54ce22a2 100644
--- a/.github/workflows/ci_ut_workflow.yml
+++ b/.github/workflows/ci_ut_workflow.yml
@@ -17,6 +17,7 @@ on:
push:
branches: [ dev, main, release/* ]
pull_request:
+ branches: [ dev, main, release/* ]
jobs:
build:
diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml
index d0923a8d5..83e6f572f 100644
--- a/.github/workflows/linter.yml
+++ b/.github/workflows/linter.yml
@@ -15,11 +15,8 @@ name: Lint Code Base
#############################
# Start the job on all push #
#############################
-on:
- workflow_dispatch:
- push:
- branches: [ dev, main, release/* ]
- pull_request:
+on: [ push, pull_request, workflow_dispatch ]
+
###############
# Set the Job #
###############
From 41427678136db0d6fbb66f08bb6a055abb98764e Mon Sep 17 00:00:00 2001
From: hallvictoria
Date: Tue, 9 Apr 2024 13:18:40 -0500
Subject: [PATCH 072/101] test
---
azure_functions_worker/bindings/meta.py | 4 +-
azure_functions_worker/dispatcher.py | 9 ++--
azure_functions_worker/http_v2.py | 2 +-
azure_functions_worker/logging.py | 4 +-
mylog.txt | 66 +++++++++++++++++++++++++
5 files changed, 77 insertions(+), 8 deletions(-)
create mode 100644 mylog.txt
diff --git a/azure_functions_worker/bindings/meta.py b/azure_functions_worker/bindings/meta.py
index b7daf5666..cfdd2b73e 100644
--- a/azure_functions_worker/bindings/meta.py
+++ b/azure_functions_worker/bindings/meta.py
@@ -22,7 +22,7 @@
def _check_http_input_type_annotation(bind_name: str, pytype: type) -> bool:
if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION and \
is_envvar_true(PYTHON_ENABLE_INIT_INDEXING):
- import azure.functions.extension.base as ext_base
+ import azurefunctions.extension.base as ext_base
if ext_base.HttpV2FeatureChecker.http_v2_enabled():
return ext_base.RequestTrackerMeta.check_type(pytype)
@@ -33,7 +33,7 @@ def _check_http_input_type_annotation(bind_name: str, pytype: type) -> bool:
def _check_http_output_type_annotation(bind_name: str, pytype: type) -> bool:
if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION and \
is_envvar_true(PYTHON_ENABLE_INIT_INDEXING):
- import azure.functions.extension.base as ext_base
+ import azurefunctions.extension.base as ext_base
if ext_base.HttpV2FeatureChecker.http_v2_enabled():
return ext_base.ResponseTrackerMeta.check_type(pytype)
diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py
index 161a74284..e38f8d427 100644
--- a/azure_functions_worker/dispatcher.py
+++ b/azure_functions_worker/dispatcher.py
@@ -316,7 +316,7 @@ async def _handle__worker_init_request(self, request):
if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION \
and self._has_http_func:
- from azure.functions.extension.base \
+ from azurefunctions.extension.base \
import HttpV2FeatureChecker
if HttpV2FeatureChecker.http_v2_enabled():
@@ -482,6 +482,7 @@ async def _handle__function_load_request(self, request):
status=protos.StatusResult.Success)))
except Exception as ex:
+ logger.warning("VICTORIA Error: {}", ex)
return protos.StreamingMessage(
request_id=self.request_id,
function_load_response=protos.FunctionLoadResponse(
@@ -543,14 +544,14 @@ async def _handle__invocation_request(self, request):
BASE_EXT_SUPPORTED_PY_MINOR_VERSION \
and fi.trigger_metadata is not None \
and fi.trigger_metadata.get('type') == HTTP_TRIGGER:
- from azure.functions.extension.base import HttpV2FeatureChecker
+ from azurefunctions.extension.base import HttpV2FeatureChecker
http_v2_enabled = HttpV2FeatureChecker.http_v2_enabled()
if http_v2_enabled:
http_request = await http_coordinator.get_http_request_async(
invocation_id)
- from azure.functions.extension.base import RequestTrackerMeta
+ from azurefunctions.extension.base import RequestTrackerMeta
route_params = {key: item.string for key, item
in trigger_metadata.items() if key not in [
'Headers', 'Query']}
@@ -703,7 +704,7 @@ async def _handle__function_environment_reload_request(self, request):
if sys.version_info.minor >= \
BASE_EXT_SUPPORTED_PY_MINOR_VERSION and \
self._has_http_func:
- from azure.functions.extension.base \
+ from azurefunctions.extension.base \
import HttpV2FeatureChecker
if HttpV2FeatureChecker.http_v2_enabled():
diff --git a/azure_functions_worker/http_v2.py b/azure_functions_worker/http_v2.py
index 91758d0a2..313fd9a3c 100644
--- a/azure_functions_worker/http_v2.py
+++ b/azure_functions_worker/http_v2.py
@@ -165,7 +165,7 @@ def get_unused_tcp_port():
def initialize_http_server():
- from azure.functions.extension.base \
+ from azurefunctions.extension.base \
import ModuleTrackerMeta, RequestTrackerMeta
web_extension_mod_name = ModuleTrackerMeta.get_module()
diff --git a/azure_functions_worker/logging.py b/azure_functions_worker/logging.py
index adb5ff294..12bc2e976 100644
--- a/azure_functions_worker/logging.py
+++ b/azure_functions_worker/logging.py
@@ -13,7 +13,7 @@
SDK_LOG_PREFIX = "azure.functions"
SYSTEM_ERROR_LOG_PREFIX = "azure_functions_worker_errors"
-
+local_handler = logging.FileHandler("C:\\Users\\victoriahall\\Documents\\repos\\azure-functions-python-worker\\mylog.txt")
logger: logging.Logger = logging.getLogger(SYSTEM_LOG_PREFIX)
error_logger: logging.Logger = (
logging.getLogger(SYSTEM_ERROR_LOG_PREFIX))
@@ -78,6 +78,8 @@ def setup(log_level, log_destination):
error_logger.addHandler(error_handler)
error_logger.setLevel(getattr(logging, log_level))
+ logger.addHandler(local_handler)
+
def disable_console_logging() -> None:
# We should only remove the sys.stdout stream, as error_logger is used for
diff --git a/mylog.txt b/mylog.txt
new file mode 100644
index 000000000..101cc22e0
--- /dev/null
+++ b/mylog.txt
@@ -0,0 +1,66 @@
+Starting Azure Functions Python Worker.
+Worker ID: 6dc9b2d4-fb85-4d7b-8da4-e154805df978, Request ID: 02e1a775-3c01-4879-a822-082610e9fe4a, Host Address: 127.0.0.1:56694
+Successfully opened gRPC channel to 127.0.0.1:56694
+Detaching console logging.
+Switched to gRPC logging.
+Received WorkerInitRequest, python version 3.11.9 (tags/v3.11.9:de54cf5, Apr 2 2024, 10:12:12) [MSC v.1938 64 bit (AMD64)], worker version 4.26.0, request ID 02e1a775-3c01-4879-a822-082610e9fe4a. App Settings state: PYTHON_THREADPOOL_THREAD_COUNT: 1000 | PYTHON_ENABLE_INIT_INDEXING: 1 | PYTHON_ENABLE_WORKER_EXTENSIONS: False. To enable debug level logging, please refer to https://aka.ms/python-enable-debug-logging
+Indexed function app and found 8 functions
+Received WorkerMetadataRequest, request ID 02e1a775-3c01-4879-a822-082610e9fe4a, function_path: C:\Users\victoriahall\Documents\repos\azure-functions-python-worker\tests\endtoend\http_functions\http_functions_v2\fastapi\function_app.py
+Starting Azure Functions Python Worker.
+Worker ID: e01fe27f-1bc0-4614-a0c7-8a04f5875953, Request ID: d4a05a61-3069-433d-806e-a1ae7b5ae34f, Host Address: 127.0.0.1:56934
+Successfully opened gRPC channel to 127.0.0.1:56934
+Detaching console logging.
+Switched to gRPC logging.
+Received WorkerInitRequest, python version 3.11.9 (tags/v3.11.9:de54cf5, Apr 2 2024, 10:12:12) [MSC v.1938 64 bit (AMD64)], worker version 4.26.0, request ID d4a05a61-3069-433d-806e-a1ae7b5ae34f. App Settings state: PYTHON_THREADPOOL_THREAD_COUNT: 1000 | PYTHON_ENABLE_WORKER_EXTENSIONS: False. To enable debug level logging, please refer to https://aka.ms/python-enable-debug-logging
+Received WorkerMetadataRequest, request ID d4a05a61-3069-433d-806e-a1ae7b5ae34f, function_path: C:\Users\victoriahall\Downloads\streaming_flask\streaming_flask\function_app.py
+Indexed function app and found 1 functions
+Starting Azure Functions Python Worker.
+Worker ID: 8dcce15c-568d-4692-aba6-e5c82f0f84d5, Request ID: 1c00731f-108f-4d91-993c-485114c64174, Host Address: 127.0.0.1:56982
+Successfully opened gRPC channel to 127.0.0.1:56982
+Detaching console logging.
+Switched to gRPC logging.
+Received WorkerInitRequest, python version 3.11.9 (tags/v3.11.9:de54cf5, Apr 2 2024, 10:12:12) [MSC v.1938 64 bit (AMD64)], worker version 4.26.0, request ID 1c00731f-108f-4d91-993c-485114c64174. App Settings state: PYTHON_THREADPOOL_THREAD_COUNT: 1000 | PYTHON_ENABLE_WORKER_EXTENSIONS: False. To enable debug level logging, please refer to https://aka.ms/python-enable-debug-logging
+Received WorkerMetadataRequest, request ID 1c00731f-108f-4d91-993c-485114c64174, function_path: C:\Users\victoriahall\Downloads\streaming_flask\streaming_flask\function_app.py
+Indexed function app and found 1 functions
+Starting Azure Functions Python Worker.
+Worker ID: 257d0ff5-8812-4187-986e-1eecc77e78f7, Request ID: f583915d-e9ed-4b28-9ae0-1965499bc9a2, Host Address: 127.0.0.1:57026
+Successfully opened gRPC channel to 127.0.0.1:57026
+Detaching console logging.
+Switched to gRPC logging.
+Received WorkerInitRequest, python version 3.11.9 (tags/v3.11.9:de54cf5, Apr 2 2024, 10:12:12) [MSC v.1938 64 bit (AMD64)], worker version 4.26.0, request ID f583915d-e9ed-4b28-9ae0-1965499bc9a2. App Settings state: PYTHON_THREADPOOL_THREAD_COUNT: 1000 | PYTHON_ENABLE_WORKER_EXTENSIONS: False. To enable debug level logging, please refer to https://aka.ms/python-enable-debug-logging
+Received WorkerMetadataRequest, request ID f583915d-e9ed-4b28-9ae0-1965499bc9a2, function_path: C:\Users\victoriahall\Downloads\streaming_flask\streaming_flask\function_app.py
+Indexed function app and found 1 functions
+Starting Azure Functions Python Worker.
+Worker ID: f5151493-68f1-4eaa-ae15-83ba85af70d5, Request ID: 060d3d04-4570-4fb8-9339-be58790c570c, Host Address: 127.0.0.1:57080
+Successfully opened gRPC channel to 127.0.0.1:57080
+Detaching console logging.
+Switched to gRPC logging.
+Received WorkerInitRequest, python version 3.11.9 (tags/v3.11.9:de54cf5, Apr 2 2024, 10:12:12) [MSC v.1938 64 bit (AMD64)], worker version 4.26.0, request ID 060d3d04-4570-4fb8-9339-be58790c570c. App Settings state: PYTHON_THREADPOOL_THREAD_COUNT: 1000 | PYTHON_ENABLE_INIT_INDEXING: 1 | PYTHON_ENABLE_WORKER_EXTENSIONS: False. To enable debug level logging, please refer to https://aka.ms/python-enable-debug-logging
+Indexed function app and found 1 functions
+Starting Azure Functions Python Worker.
+Worker ID: f5f6e191-ae39-4968-bc17-5a813ada1c38, Request ID: c14a058e-19cd-4ebc-a4a9-da6f4fb2f377, Host Address: 127.0.0.1:57116
+Successfully opened gRPC channel to 127.0.0.1:57116
+Detaching console logging.
+Switched to gRPC logging.
+Received WorkerInitRequest, python version 3.11.9 (tags/v3.11.9:de54cf5, Apr 2 2024, 10:12:12) [MSC v.1938 64 bit (AMD64)], worker version 4.26.0, request ID c14a058e-19cd-4ebc-a4a9-da6f4fb2f377. App Settings state: PYTHON_THREADPOOL_THREAD_COUNT: 1000 | PYTHON_ENABLE_INIT_INDEXING: 1 | PYTHON_ENABLE_WORKER_EXTENSIONS: False. To enable debug level logging, please refer to https://aka.ms/python-enable-debug-logging
+Indexed function app and found 1 functions
+Starting Azure Functions Python Worker.
+Worker ID: 35186346-38de-491c-b07d-65525818a2a1, Request ID: 60aea45d-b492-436d-b695-75ec7d99b16a, Host Address: 127.0.0.1:57434
+Successfully opened gRPC channel to 127.0.0.1:57434
+Detaching console logging.
+Switched to gRPC logging.
+Received WorkerInitRequest, python version 3.11.9 (tags/v3.11.9:de54cf5, Apr 2 2024, 10:12:12) [MSC v.1938 64 bit (AMD64)], worker version 4.26.0, request ID 60aea45d-b492-436d-b695-75ec7d99b16a. App Settings state: PYTHON_THREADPOOL_THREAD_COUNT: 1000 | PYTHON_ENABLE_INIT_INDEXING: 1 | PYTHON_ENABLE_WORKER_EXTENSIONS: False. To enable debug level logging, please refer to https://aka.ms/python-enable-debug-logging
+Indexed function app and found 1 functions
+Starting Azure Functions Python Worker.
+Worker ID: ad247ea9-b5d4-46a6-a5a5-b52271648e00, Request ID: 612075e4-49d1-47e1-902e-a83b86bfa1da, Host Address: 127.0.0.1:57547
+Successfully opened gRPC channel to 127.0.0.1:57547
+Detaching console logging.
+Switched to gRPC logging.
+Received WorkerInitRequest, python version 3.11.9 (tags/v3.11.9:de54cf5, Apr 2 2024, 10:12:12) [MSC v.1938 64 bit (AMD64)], worker version 4.26.0, request ID 612075e4-49d1-47e1-902e-a83b86bfa1da. App Settings state: PYTHON_THREADPOOL_THREAD_COUNT: 1000 | PYTHON_ENABLE_INIT_INDEXING: 1 | PYTHON_ENABLE_WORKER_EXTENSIONS: False. To enable debug level logging, please refer to https://aka.ms/python-enable-debug-logging
+Starting Azure Functions Python Worker.
+Worker ID: 0780177e-191a-4211-a1a3-1cca83e42d4a, Request ID: ec727f1f-4cde-4225-a53f-44fa09247368, Host Address: 127.0.0.1:57776
+Successfully opened gRPC channel to 127.0.0.1:57776
+Detaching console logging.
+Switched to gRPC logging.
+Received WorkerInitRequest, python version 3.11.9 (tags/v3.11.9:de54cf5, Apr 2 2024, 10:12:12) [MSC v.1938 64 bit (AMD64)], worker version 4.26.0, request ID ec727f1f-4cde-4225-a53f-44fa09247368. App Settings state: PYTHON_THREADPOOL_THREAD_COUNT: 1000 | PYTHON_ENABLE_INIT_INDEXING: 1 | PYTHON_ENABLE_WORKER_EXTENSIONS: False. To enable debug level logging, please refer to https://aka.ms/python-enable-debug-logging
+Indexed function app and found 1 functions
From 3915493a03ea4a5a15b904816370ff346e7921ad Mon Sep 17 00:00:00 2001
From: hallvictoria
Date: Tue, 9 Apr 2024 13:22:52 -0500
Subject: [PATCH 073/101] Revert "test"
This reverts commit 41427678136db0d6fbb66f08bb6a055abb98764e.
---
azure_functions_worker/bindings/meta.py | 4 +-
azure_functions_worker/dispatcher.py | 9 ++--
azure_functions_worker/http_v2.py | 2 +-
azure_functions_worker/logging.py | 4 +-
mylog.txt | 66 -------------------------
5 files changed, 8 insertions(+), 77 deletions(-)
delete mode 100644 mylog.txt
diff --git a/azure_functions_worker/bindings/meta.py b/azure_functions_worker/bindings/meta.py
index cfdd2b73e..b7daf5666 100644
--- a/azure_functions_worker/bindings/meta.py
+++ b/azure_functions_worker/bindings/meta.py
@@ -22,7 +22,7 @@
def _check_http_input_type_annotation(bind_name: str, pytype: type) -> bool:
if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION and \
is_envvar_true(PYTHON_ENABLE_INIT_INDEXING):
- import azurefunctions.extension.base as ext_base
+ import azure.functions.extension.base as ext_base
if ext_base.HttpV2FeatureChecker.http_v2_enabled():
return ext_base.RequestTrackerMeta.check_type(pytype)
@@ -33,7 +33,7 @@ def _check_http_input_type_annotation(bind_name: str, pytype: type) -> bool:
def _check_http_output_type_annotation(bind_name: str, pytype: type) -> bool:
if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION and \
is_envvar_true(PYTHON_ENABLE_INIT_INDEXING):
- import azurefunctions.extension.base as ext_base
+ import azure.functions.extension.base as ext_base
if ext_base.HttpV2FeatureChecker.http_v2_enabled():
return ext_base.ResponseTrackerMeta.check_type(pytype)
diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py
index e38f8d427..161a74284 100644
--- a/azure_functions_worker/dispatcher.py
+++ b/azure_functions_worker/dispatcher.py
@@ -316,7 +316,7 @@ async def _handle__worker_init_request(self, request):
if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION \
and self._has_http_func:
- from azurefunctions.extension.base \
+ from azure.functions.extension.base \
import HttpV2FeatureChecker
if HttpV2FeatureChecker.http_v2_enabled():
@@ -482,7 +482,6 @@ async def _handle__function_load_request(self, request):
status=protos.StatusResult.Success)))
except Exception as ex:
- logger.warning("VICTORIA Error: {}", ex)
return protos.StreamingMessage(
request_id=self.request_id,
function_load_response=protos.FunctionLoadResponse(
@@ -544,14 +543,14 @@ async def _handle__invocation_request(self, request):
BASE_EXT_SUPPORTED_PY_MINOR_VERSION \
and fi.trigger_metadata is not None \
and fi.trigger_metadata.get('type') == HTTP_TRIGGER:
- from azurefunctions.extension.base import HttpV2FeatureChecker
+ from azure.functions.extension.base import HttpV2FeatureChecker
http_v2_enabled = HttpV2FeatureChecker.http_v2_enabled()
if http_v2_enabled:
http_request = await http_coordinator.get_http_request_async(
invocation_id)
- from azurefunctions.extension.base import RequestTrackerMeta
+ from azure.functions.extension.base import RequestTrackerMeta
route_params = {key: item.string for key, item
in trigger_metadata.items() if key not in [
'Headers', 'Query']}
@@ -704,7 +703,7 @@ async def _handle__function_environment_reload_request(self, request):
if sys.version_info.minor >= \
BASE_EXT_SUPPORTED_PY_MINOR_VERSION and \
self._has_http_func:
- from azurefunctions.extension.base \
+ from azure.functions.extension.base \
import HttpV2FeatureChecker
if HttpV2FeatureChecker.http_v2_enabled():
diff --git a/azure_functions_worker/http_v2.py b/azure_functions_worker/http_v2.py
index 313fd9a3c..91758d0a2 100644
--- a/azure_functions_worker/http_v2.py
+++ b/azure_functions_worker/http_v2.py
@@ -165,7 +165,7 @@ def get_unused_tcp_port():
def initialize_http_server():
- from azurefunctions.extension.base \
+ from azure.functions.extension.base \
import ModuleTrackerMeta, RequestTrackerMeta
web_extension_mod_name = ModuleTrackerMeta.get_module()
diff --git a/azure_functions_worker/logging.py b/azure_functions_worker/logging.py
index 12bc2e976..adb5ff294 100644
--- a/azure_functions_worker/logging.py
+++ b/azure_functions_worker/logging.py
@@ -13,7 +13,7 @@
SDK_LOG_PREFIX = "azure.functions"
SYSTEM_ERROR_LOG_PREFIX = "azure_functions_worker_errors"
-local_handler = logging.FileHandler("C:\\Users\\victoriahall\\Documents\\repos\\azure-functions-python-worker\\mylog.txt")
+
logger: logging.Logger = logging.getLogger(SYSTEM_LOG_PREFIX)
error_logger: logging.Logger = (
logging.getLogger(SYSTEM_ERROR_LOG_PREFIX))
@@ -78,8 +78,6 @@ def setup(log_level, log_destination):
error_logger.addHandler(error_handler)
error_logger.setLevel(getattr(logging, log_level))
- logger.addHandler(local_handler)
-
def disable_console_logging() -> None:
# We should only remove the sys.stdout stream, as error_logger is used for
diff --git a/mylog.txt b/mylog.txt
deleted file mode 100644
index 101cc22e0..000000000
--- a/mylog.txt
+++ /dev/null
@@ -1,66 +0,0 @@
-Starting Azure Functions Python Worker.
-Worker ID: 6dc9b2d4-fb85-4d7b-8da4-e154805df978, Request ID: 02e1a775-3c01-4879-a822-082610e9fe4a, Host Address: 127.0.0.1:56694
-Successfully opened gRPC channel to 127.0.0.1:56694
-Detaching console logging.
-Switched to gRPC logging.
-Received WorkerInitRequest, python version 3.11.9 (tags/v3.11.9:de54cf5, Apr 2 2024, 10:12:12) [MSC v.1938 64 bit (AMD64)], worker version 4.26.0, request ID 02e1a775-3c01-4879-a822-082610e9fe4a. App Settings state: PYTHON_THREADPOOL_THREAD_COUNT: 1000 | PYTHON_ENABLE_INIT_INDEXING: 1 | PYTHON_ENABLE_WORKER_EXTENSIONS: False. To enable debug level logging, please refer to https://aka.ms/python-enable-debug-logging
-Indexed function app and found 8 functions
-Received WorkerMetadataRequest, request ID 02e1a775-3c01-4879-a822-082610e9fe4a, function_path: C:\Users\victoriahall\Documents\repos\azure-functions-python-worker\tests\endtoend\http_functions\http_functions_v2\fastapi\function_app.py
-Starting Azure Functions Python Worker.
-Worker ID: e01fe27f-1bc0-4614-a0c7-8a04f5875953, Request ID: d4a05a61-3069-433d-806e-a1ae7b5ae34f, Host Address: 127.0.0.1:56934
-Successfully opened gRPC channel to 127.0.0.1:56934
-Detaching console logging.
-Switched to gRPC logging.
-Received WorkerInitRequest, python version 3.11.9 (tags/v3.11.9:de54cf5, Apr 2 2024, 10:12:12) [MSC v.1938 64 bit (AMD64)], worker version 4.26.0, request ID d4a05a61-3069-433d-806e-a1ae7b5ae34f. App Settings state: PYTHON_THREADPOOL_THREAD_COUNT: 1000 | PYTHON_ENABLE_WORKER_EXTENSIONS: False. To enable debug level logging, please refer to https://aka.ms/python-enable-debug-logging
-Received WorkerMetadataRequest, request ID d4a05a61-3069-433d-806e-a1ae7b5ae34f, function_path: C:\Users\victoriahall\Downloads\streaming_flask\streaming_flask\function_app.py
-Indexed function app and found 1 functions
-Starting Azure Functions Python Worker.
-Worker ID: 8dcce15c-568d-4692-aba6-e5c82f0f84d5, Request ID: 1c00731f-108f-4d91-993c-485114c64174, Host Address: 127.0.0.1:56982
-Successfully opened gRPC channel to 127.0.0.1:56982
-Detaching console logging.
-Switched to gRPC logging.
-Received WorkerInitRequest, python version 3.11.9 (tags/v3.11.9:de54cf5, Apr 2 2024, 10:12:12) [MSC v.1938 64 bit (AMD64)], worker version 4.26.0, request ID 1c00731f-108f-4d91-993c-485114c64174. App Settings state: PYTHON_THREADPOOL_THREAD_COUNT: 1000 | PYTHON_ENABLE_WORKER_EXTENSIONS: False. To enable debug level logging, please refer to https://aka.ms/python-enable-debug-logging
-Received WorkerMetadataRequest, request ID 1c00731f-108f-4d91-993c-485114c64174, function_path: C:\Users\victoriahall\Downloads\streaming_flask\streaming_flask\function_app.py
-Indexed function app and found 1 functions
-Starting Azure Functions Python Worker.
-Worker ID: 257d0ff5-8812-4187-986e-1eecc77e78f7, Request ID: f583915d-e9ed-4b28-9ae0-1965499bc9a2, Host Address: 127.0.0.1:57026
-Successfully opened gRPC channel to 127.0.0.1:57026
-Detaching console logging.
-Switched to gRPC logging.
-Received WorkerInitRequest, python version 3.11.9 (tags/v3.11.9:de54cf5, Apr 2 2024, 10:12:12) [MSC v.1938 64 bit (AMD64)], worker version 4.26.0, request ID f583915d-e9ed-4b28-9ae0-1965499bc9a2. App Settings state: PYTHON_THREADPOOL_THREAD_COUNT: 1000 | PYTHON_ENABLE_WORKER_EXTENSIONS: False. To enable debug level logging, please refer to https://aka.ms/python-enable-debug-logging
-Received WorkerMetadataRequest, request ID f583915d-e9ed-4b28-9ae0-1965499bc9a2, function_path: C:\Users\victoriahall\Downloads\streaming_flask\streaming_flask\function_app.py
-Indexed function app and found 1 functions
-Starting Azure Functions Python Worker.
-Worker ID: f5151493-68f1-4eaa-ae15-83ba85af70d5, Request ID: 060d3d04-4570-4fb8-9339-be58790c570c, Host Address: 127.0.0.1:57080
-Successfully opened gRPC channel to 127.0.0.1:57080
-Detaching console logging.
-Switched to gRPC logging.
-Received WorkerInitRequest, python version 3.11.9 (tags/v3.11.9:de54cf5, Apr 2 2024, 10:12:12) [MSC v.1938 64 bit (AMD64)], worker version 4.26.0, request ID 060d3d04-4570-4fb8-9339-be58790c570c. App Settings state: PYTHON_THREADPOOL_THREAD_COUNT: 1000 | PYTHON_ENABLE_INIT_INDEXING: 1 | PYTHON_ENABLE_WORKER_EXTENSIONS: False. To enable debug level logging, please refer to https://aka.ms/python-enable-debug-logging
-Indexed function app and found 1 functions
-Starting Azure Functions Python Worker.
-Worker ID: f5f6e191-ae39-4968-bc17-5a813ada1c38, Request ID: c14a058e-19cd-4ebc-a4a9-da6f4fb2f377, Host Address: 127.0.0.1:57116
-Successfully opened gRPC channel to 127.0.0.1:57116
-Detaching console logging.
-Switched to gRPC logging.
-Received WorkerInitRequest, python version 3.11.9 (tags/v3.11.9:de54cf5, Apr 2 2024, 10:12:12) [MSC v.1938 64 bit (AMD64)], worker version 4.26.0, request ID c14a058e-19cd-4ebc-a4a9-da6f4fb2f377. App Settings state: PYTHON_THREADPOOL_THREAD_COUNT: 1000 | PYTHON_ENABLE_INIT_INDEXING: 1 | PYTHON_ENABLE_WORKER_EXTENSIONS: False. To enable debug level logging, please refer to https://aka.ms/python-enable-debug-logging
-Indexed function app and found 1 functions
-Starting Azure Functions Python Worker.
-Worker ID: 35186346-38de-491c-b07d-65525818a2a1, Request ID: 60aea45d-b492-436d-b695-75ec7d99b16a, Host Address: 127.0.0.1:57434
-Successfully opened gRPC channel to 127.0.0.1:57434
-Detaching console logging.
-Switched to gRPC logging.
-Received WorkerInitRequest, python version 3.11.9 (tags/v3.11.9:de54cf5, Apr 2 2024, 10:12:12) [MSC v.1938 64 bit (AMD64)], worker version 4.26.0, request ID 60aea45d-b492-436d-b695-75ec7d99b16a. App Settings state: PYTHON_THREADPOOL_THREAD_COUNT: 1000 | PYTHON_ENABLE_INIT_INDEXING: 1 | PYTHON_ENABLE_WORKER_EXTENSIONS: False. To enable debug level logging, please refer to https://aka.ms/python-enable-debug-logging
-Indexed function app and found 1 functions
-Starting Azure Functions Python Worker.
-Worker ID: ad247ea9-b5d4-46a6-a5a5-b52271648e00, Request ID: 612075e4-49d1-47e1-902e-a83b86bfa1da, Host Address: 127.0.0.1:57547
-Successfully opened gRPC channel to 127.0.0.1:57547
-Detaching console logging.
-Switched to gRPC logging.
-Received WorkerInitRequest, python version 3.11.9 (tags/v3.11.9:de54cf5, Apr 2 2024, 10:12:12) [MSC v.1938 64 bit (AMD64)], worker version 4.26.0, request ID 612075e4-49d1-47e1-902e-a83b86bfa1da. App Settings state: PYTHON_THREADPOOL_THREAD_COUNT: 1000 | PYTHON_ENABLE_INIT_INDEXING: 1 | PYTHON_ENABLE_WORKER_EXTENSIONS: False. To enable debug level logging, please refer to https://aka.ms/python-enable-debug-logging
-Starting Azure Functions Python Worker.
-Worker ID: 0780177e-191a-4211-a1a3-1cca83e42d4a, Request ID: ec727f1f-4cde-4225-a53f-44fa09247368, Host Address: 127.0.0.1:57776
-Successfully opened gRPC channel to 127.0.0.1:57776
-Detaching console logging.
-Switched to gRPC logging.
-Received WorkerInitRequest, python version 3.11.9 (tags/v3.11.9:de54cf5, Apr 2 2024, 10:12:12) [MSC v.1938 64 bit (AMD64)], worker version 4.26.0, request ID ec727f1f-4cde-4225-a53f-44fa09247368. App Settings state: PYTHON_THREADPOOL_THREAD_COUNT: 1000 | PYTHON_ENABLE_INIT_INDEXING: 1 | PYTHON_ENABLE_WORKER_EXTENSIONS: False. To enable debug level logging, please refer to https://aka.ms/python-enable-debug-logging
-Indexed function app and found 1 functions
From ec42efcd6c7e17251dfc5c5413acc0e345deb061 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Tue, 9 Apr 2024 17:50:37 -0700
Subject: [PATCH 074/101] address feedback
---
azure_functions_worker/bindings/meta.py | 19 +---
azure_functions_worker/constants.py | 3 -
azure_functions_worker/dispatcher.py | 106 ++++++++----------
azure_functions_worker/functions.py | 12 ++
azure_functions_worker/http_v2.py | 60 ++++++++--
.../test_enable_debug_logging_functions.py | 4 +-
6 files changed, 116 insertions(+), 88 deletions(-)
diff --git a/azure_functions_worker/bindings/meta.py b/azure_functions_worker/bindings/meta.py
index b7daf5666..9d70160bf 100644
--- a/azure_functions_worker/bindings/meta.py
+++ b/azure_functions_worker/bindings/meta.py
@@ -9,9 +9,7 @@
from . import datumdef
from . import generic
from .shared_memory_data_transfer import SharedMemoryManager
-from ..constants import BASE_EXT_SUPPORTED_PY_MINOR_VERSION, \
- PYTHON_ENABLE_INIT_INDEXING
-from ..utils.common import is_envvar_true
+from ..http_v2 import HttpV2Registry
PB_TYPE = 'rpc_data'
PB_TYPE_DATA = 'data'
@@ -20,22 +18,17 @@
def _check_http_input_type_annotation(bind_name: str, pytype: type) -> bool:
- if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION and \
- is_envvar_true(PYTHON_ENABLE_INIT_INDEXING):
- import azure.functions.extension.base as ext_base
- if ext_base.HttpV2FeatureChecker.http_v2_enabled():
- return ext_base.RequestTrackerMeta.check_type(pytype)
+ if HttpV2Registry.http_v2_enabled():
+ return HttpV2Registry.ext_base().RequestTrackerMeta \
+ .check_type(pytype)
binding = get_binding(bind_name)
return binding.check_input_type_annotation(pytype)
def _check_http_output_type_annotation(bind_name: str, pytype: type) -> bool:
- if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION and \
- is_envvar_true(PYTHON_ENABLE_INIT_INDEXING):
- import azure.functions.extension.base as ext_base
- if ext_base.HttpV2FeatureChecker.http_v2_enabled():
- return ext_base.ResponseTrackerMeta.check_type(pytype)
+ if HttpV2Registry.http_v2_enabled():
+ return HttpV2Registry.ext_base().ResponseTrackerMeta.check_type(pytype)
binding = get_binding(bind_name)
return binding.check_output_type_annotation(pytype)
diff --git a/azure_functions_worker/constants.py b/azure_functions_worker/constants.py
index eea0193ec..8820b4e98 100644
--- a/azure_functions_worker/constants.py
+++ b/azure_functions_worker/constants.py
@@ -63,9 +63,6 @@
METADATA_PROPERTIES_WORKER_INDEXED = "worker_indexed"
-# HostNames
-LOCAL_HOST = "127.0.0.1"
-
# Header names
X_MS_INVOCATION_ID = "x-ms-invocation-id"
diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py
index 161a74284..65c9ebd57 100644
--- a/azure_functions_worker/dispatcher.py
+++ b/azure_functions_worker/dispatcher.py
@@ -21,7 +21,7 @@
import grpc
from . import bindings, constants, functions, loader, protos
from .bindings.shared_memory_data_transfer import SharedMemoryManager
-from .constants import (HTTP_TRIGGER, PYTHON_ROLLBACK_CWD_PATH,
+from .constants import (PYTHON_ROLLBACK_CWD_PATH,
PYTHON_THREADPOOL_THREAD_COUNT,
PYTHON_THREADPOOL_THREAD_COUNT_DEFAULT,
PYTHON_THREADPOOL_THREAD_COUNT_MAX_37,
@@ -30,10 +30,9 @@
PYTHON_SCRIPT_FILE_NAME,
PYTHON_SCRIPT_FILE_NAME_DEFAULT,
PYTHON_LANGUAGE_RUNTIME, PYTHON_ENABLE_INIT_INDEXING,
- METADATA_PROPERTIES_WORKER_INDEXED,
- BASE_EXT_SUPPORTED_PY_MINOR_VERSION)
+ METADATA_PROPERTIES_WORKER_INDEXED)
from .extension import ExtensionManager
-from .http_v2 import http_coordinator, initialize_http_server
+from .http_v2 import http_coordinator, initialize_http_server, HttpV2Registry
from .logging import disable_console_logging, enable_console_logging
from .logging import (logger, error_logger, is_system_log_category,
CONSOLE_LOG_PREFIX, format_exception)
@@ -75,7 +74,6 @@ def __init__(self, loop: BaseEventLoop, host: str, port: int,
self._functions = functions.Registry()
self._shmem_mgr = SharedMemoryManager()
self._old_task_factory = None
- self._has_http_func = False
# Used to store metadata returns
self._function_metadata_result = None
@@ -314,14 +312,19 @@ async def _handle__worker_init_request(self, request):
except Exception as ex:
self._function_metadata_exception = ex
- if sys.version_info.minor >= BASE_EXT_SUPPORTED_PY_MINOR_VERSION \
- and self._has_http_func:
- from azure.functions.extension.base \
- import HttpV2FeatureChecker
-
- if HttpV2FeatureChecker.http_v2_enabled():
+ try:
+ if HttpV2Registry.http_v2_enabled(self._functions):
capabilities[constants.HTTP_URI] = \
- initialize_http_server()
+ initialize_http_server(self._host)
+ except Exception as ex:
+ return protos.StreamingMessage(
+ request_id=self.request_id,
+ worker_init_response=protos.WorkerInitResponse(
+ result=protos.StatusResult(
+ status=protos.StatusResult.Failure,
+ exception=self._serialize_exception(ex))
+ )
+ )
return protos.StreamingMessage(
request_id=self.request_id,
@@ -493,7 +496,6 @@ async def _handle__function_load_request(self, request):
async def _handle__invocation_request(self, request):
invocation_time = datetime.utcnow()
invoc_request = request.invocation_request
- trigger_metadata = invoc_request.trigger_metadata
invocation_id = invoc_request.invocation_id
function_id = invoc_request.function_id
@@ -528,9 +530,10 @@ async def _handle__invocation_request(self, request):
for pb in invoc_request.input_data:
pb_type_info = fi.input_types[pb.name]
- trigger_metadata = None
if bindings.is_trigger_binding(pb_type_info.binding_name):
trigger_metadata = invoc_request.trigger_metadata
+ else:
+ trigger_metadata = None
args[pb.name] = bindings.from_incoming_proto(
pb_type_info.binding_name, pb,
@@ -538,24 +541,20 @@ async def _handle__invocation_request(self, request):
pytype=pb_type_info.pytype,
shmem_mgr=self._shmem_mgr)
- http_v2_enabled = False
- if sys.version_info.minor >= \
- BASE_EXT_SUPPORTED_PY_MINOR_VERSION \
- and fi.trigger_metadata is not None \
- and fi.trigger_metadata.get('type') == HTTP_TRIGGER:
- from azure.functions.extension.base import HttpV2FeatureChecker
- http_v2_enabled = HttpV2FeatureChecker.http_v2_enabled()
+ http_v2_enabled = self._functions.get_function(function_id) \
+ .is_http_func and \
+ HttpV2Registry.http_v2_enabled()
if http_v2_enabled:
http_request = await http_coordinator.get_http_request_async(
invocation_id)
- from azure.functions.extension.base import RequestTrackerMeta
route_params = {key: item.string for key, item
- in trigger_metadata.items() if key not in [
- 'Headers', 'Query']}
+ in invoc_request.trigger_metadata.items()
+ if key not in ['Headers', 'Query']}
- (RequestTrackerMeta.get_synchronizer()
+ (HttpV2Registry.ext_base().RequestTrackerMeta
+ .get_synchronizer()
.sync_route_params(http_request, route_params))
args[fi.trigger_metadata.get('param_name')] = http_request
@@ -572,30 +571,22 @@ async def _handle__invocation_request(self, request):
for name in fi.output_types:
args[name] = bindings.Out()
- call_result = None
- call_error = None
- try:
- if fi.is_async:
- call_result = \
- await self._run_async_func(fi_context, fi.func, args)
- else:
- call_result = await self._loop.run_in_executor(
- self._sync_call_tp,
- self._run_sync_func,
- invocation_id, fi_context, fi.func, args)
-
- if call_result is not None and not fi.has_return:
- raise RuntimeError(
- f'function {fi.name!r} without a $return binding'
- 'returned a non-None value')
- except Exception as e:
- call_error = e
- raise
- finally:
- if http_v2_enabled:
- http_coordinator.set_http_response(
- invocation_id, call_result
- if call_result is not None else call_error)
+ if fi.is_async:
+ call_result = \
+ await self._run_async_func(fi_context, fi.func, args)
+ else:
+ call_result = await self._loop.run_in_executor(
+ self._sync_call_tp,
+ self._run_sync_func,
+ invocation_id, fi_context, fi.func, args)
+
+ if call_result is not None and not fi.has_return:
+ raise RuntimeError(
+ f'function {fi.name!r} without a $return binding'
+ 'returned a non-None value')
+
+ if http_v2_enabled:
+ http_coordinator.set_http_response(invocation_id, call_result)
output_data = []
cache_enabled = self._function_data_cache_enabled
@@ -635,6 +626,9 @@ async def _handle__invocation_request(self, request):
output_data=output_data))
except Exception as ex:
+ if http_v2_enabled:
+ http_coordinator.set_http_response(invocation_id, ex)
+
return protos.StreamingMessage(
request_id=self.request_id,
invocation_response=protos.InvocationResponse(
@@ -700,15 +694,9 @@ async def _handle__function_environment_reload_request(self, request):
except Exception as ex:
self._function_metadata_exception = ex
- if sys.version_info.minor >= \
- BASE_EXT_SUPPORTED_PY_MINOR_VERSION and \
- self._has_http_func:
- from azure.functions.extension.base \
- import HttpV2FeatureChecker
-
- if HttpV2FeatureChecker.http_v2_enabled():
- capabilities[constants.HTTP_URI] = \
- initialize_http_server()
+ if HttpV2Registry.http_v2_enabled(self._functions):
+ capabilities[constants.HTTP_URI] = \
+ initialize_http_server(self._host)
# Change function app directory
if getattr(func_env_reload_request,
@@ -750,8 +738,6 @@ def index_functions(self, function_path: str):
indexed_function_logs: List[str] = []
for func in indexed_functions:
- self._has_http_func = self._has_http_func or \
- func.is_http_function()
function_log = "Function Name: {}, Function Binding: {}" \
.format(func.get_function_name(),
[(binding.type, binding.name) for binding in
diff --git a/azure_functions_worker/functions.py b/azure_functions_worker/functions.py
index c5f00e040..acdb4f30b 100644
--- a/azure_functions_worker/functions.py
+++ b/azure_functions_worker/functions.py
@@ -28,6 +28,7 @@ class FunctionInfo(typing.NamedTuple):
requires_context: bool
is_async: bool
has_return: bool
+ is_http_func: bool
input_types: typing.Mapping[str, ParamTypeInfo]
output_types: typing.Mapping[str, ParamTypeInfo]
@@ -45,6 +46,7 @@ def __init__(self, function_name: str, msg: str) -> None:
class Registry:
_functions: typing.MutableMapping[str, FunctionInfo]
+ _has_http_func: bool = False
def __init__(self) -> None:
self._functions = {}
@@ -55,6 +57,9 @@ def get_function(self, function_id: str) -> FunctionInfo:
return None
+ def has_http_func(self) -> bool:
+ return self._has_http_func
+
@staticmethod
def get_explicit_and_implicit_return(binding_name: str,
binding: BindingInfo,
@@ -308,11 +313,13 @@ def add_func_to_registry_and_return_funcinfo(self, function,
)
trigger_metadata = None
+ is_http_func = False
if http_trigger_param_name is not None:
trigger_metadata = {
"type": HTTP_TRIGGER,
"param_name": http_trigger_param_name
}
+ is_http_func = True
function_info = FunctionInfo(
func=function,
@@ -322,12 +329,17 @@ def add_func_to_registry_and_return_funcinfo(self, function,
requires_context=requires_context,
is_async=inspect.iscoroutinefunction(function),
has_return=has_explicit_return or has_implicit_return,
+ is_http_func=is_http_func,
input_types=input_types,
output_types=output_types,
return_type=return_type,
trigger_metadata=trigger_metadata)
self._functions[function_id] = function_info
+
+ if not self._has_http_func:
+ self._has_http_func = function_info.is_http_func
+
return function_info
def add_function(self, function_id: str,
diff --git a/azure_functions_worker/http_v2.py b/azure_functions_worker/http_v2.py
index 91758d0a2..52e993735 100644
--- a/azure_functions_worker/http_v2.py
+++ b/azure_functions_worker/http_v2.py
@@ -2,10 +2,13 @@
import asyncio
import importlib
import socket
+import sys
from typing import Dict
-from azure_functions_worker.constants import X_MS_INVOCATION_ID, LOCAL_HOST
+from azure_functions_worker.constants import X_MS_INVOCATION_ID, \
+ BASE_EXT_SUPPORTED_PY_MINOR_VERSION, PYTHON_ENABLE_INIT_INDEXING
from azure_functions_worker.logging import logger
+from azure_functions_worker.utils.common import is_envvar_false
class BaseContextReference(abc.ABC):
@@ -164,11 +167,9 @@ def get_unused_tcp_port():
return port
-def initialize_http_server():
- from azure.functions.extension.base \
- import ModuleTrackerMeta, RequestTrackerMeta
-
- web_extension_mod_name = ModuleTrackerMeta.get_module()
+def initialize_http_server(host_addr, **kwargs):
+ ext_base = HttpV2Registry.ext_base()
+ web_extension_mod_name = ext_base.ModuleTrackerMeta.get_module()
extension_module = importlib.import_module(web_extension_mod_name)
web_app_class = extension_module.WebApp
web_server_class = extension_module.WebServer
@@ -176,7 +177,7 @@ def initialize_http_server():
unused_port = get_unused_tcp_port()
app = web_app_class()
- request_type = RequestTrackerMeta.get_request_type()
+ request_type = ext_base.RequestTrackerMeta.get_request_type()
@app.route
async def catch_all(request: request_type): # type: ignore
@@ -195,16 +196,57 @@ async def catch_all(request: request_type): # type: ignore
return http_resp
- web_server = web_server_class(LOCAL_HOST, unused_port, app)
+ web_server = web_server_class(host_addr, unused_port, app)
web_server_run_task = web_server.serve()
loop = asyncio.get_event_loop()
loop.create_task(web_server_run_task)
- web_server_address = f"http://{LOCAL_HOST}:{unused_port}"
+ web_server_address = f"http://{host_addr}:{unused_port}"
logger.info(f'HTTP server starting on {web_server_address}')
return web_server_address
+class HttpV2Registry:
+ _http_v2_enabled = False
+ _ext_base = None
+ _http_v2_enabled_checked = False
+
+ @classmethod
+ def http_v2_enabled(cls, functions=None, **kwargs):
+ # Check if HTTP/2 enablement has already been checked
+ if not cls._http_v2_enabled_checked:
+ # If not checked yet, mark as checked
+ cls._http_v2_enabled_checked = True
+
+ # Check if there are functions provided and if any of them has
+ # HTTP triggers
+ cls._http_v2_enabled = functions is not None and \
+ functions.has_http_func()
+
+ # If HTTP functions are present, perform additional checks
+ if cls._http_v2_enabled:
+ # Check if HTTP/2 is enabled
+ cls._http_v2_enabled = cls._check_http_v2_enabled()
+
+ # Return the result of HTTP/2 enablement
+ return cls._http_v2_enabled
+
+ @classmethod
+ def ext_base(cls):
+ return cls._ext_base
+
+ @classmethod
+ def _check_http_v2_enabled(cls):
+ if sys.version_info.minor < BASE_EXT_SUPPORTED_PY_MINOR_VERSION or \
+ is_envvar_false(PYTHON_ENABLE_INIT_INDEXING):
+ return False
+
+ import azure.functions.extension.base as ext_base
+ cls._ext_base = ext_base
+
+ return cls._ext_base.HttpV2FeatureChecker.http_v2_enabled()
+
+
http_coordinator = HttpCoordinator()
diff --git a/tests/unittests/test_enable_debug_logging_functions.py b/tests/unittests/test_enable_debug_logging_functions.py
index 6f3739809..120d54dfe 100644
--- a/tests/unittests/test_enable_debug_logging_functions.py
+++ b/tests/unittests/test_enable_debug_logging_functions.py
@@ -65,7 +65,6 @@ class TestDebugLoggingDisabledFunctions(testutils.WebHostTestCase):
"""
@classmethod
def setUpClass(cls):
- cls._pre_env = dict(os.environ)
os_environ = os.environ.copy()
os_environ[PYTHON_ENABLE_DEBUG_LOGGING] = '0'
cls._patch_environ = patch.dict('os.environ', os_environ)
@@ -74,9 +73,8 @@ def setUpClass(cls):
@classmethod
def tearDownClass(cls):
+ os.environ.pop(PYTHON_ENABLE_DEBUG_LOGGING)
super().tearDownClass()
- os.environ.clear()
- os.environ.update(cls._pre_env)
cls._patch_environ.stop()
@classmethod
From 7333a025768b03beee62f4362d4133e012e3c096 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Tue, 9 Apr 2024 23:46:21 -0700
Subject: [PATCH 075/101] revert ut ppl
---
.github/workflows/ci_ut_workflow.yml | 11 ++---------
1 file changed, 2 insertions(+), 9 deletions(-)
diff --git a/.github/workflows/ci_ut_workflow.yml b/.github/workflows/ci_ut_workflow.yml
index a54ce22a2..01aa36070 100644
--- a/.github/workflows/ci_ut_workflow.yml
+++ b/.github/workflows/ci_ut_workflow.yml
@@ -89,13 +89,12 @@ jobs:
run: |
python -m pytest -q -n auto --dist loadfile --reruns 4 --instafail --cov=./azure_functions_worker --cov-report xml --cov-branch tests/unittests
- name: Codecov
- uses: codecov/codecov-action@v4
+ uses: codecov/codecov-action@v3
with:
file: ./coverage.xml # optional
flags: unittests # optional
name: codecov # optional
fail_ci_if_error: false # optional (default = false)
- token: ${{ secrets.CODECOV_TOKEN }}
- name: Publish Logs to Artifact
if: failure()
uses: actions/upload-artifact@v4
@@ -103,10 +102,4 @@ jobs:
name: Test WebHost Logs ${{ github.run_id }} ${{ matrix.python-version }}
path: logs/*.log
if-no-files-found: ignore
- - name: Publish replays to Artifact
- if: failure()
- uses: actions/upload-artifact@v4
- with:
- name: Test Replays ${{ github.run_id }} ${{ matrix.python-version }}
- path: build/tests/replay
- if-no-files-found: ignore
+
From 99a47a210c9ac1d76270957f7226c94625ceac36 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Wed, 10 Apr 2024 01:07:50 -0700
Subject: [PATCH 076/101] skip checking has http func
---
azure_functions_worker/functions.py | 6 ------
azure_functions_worker/http_v2.py | 10 +---------
2 files changed, 1 insertion(+), 15 deletions(-)
diff --git a/azure_functions_worker/functions.py b/azure_functions_worker/functions.py
index acdb4f30b..10bf70968 100644
--- a/azure_functions_worker/functions.py
+++ b/azure_functions_worker/functions.py
@@ -46,7 +46,6 @@ def __init__(self, function_name: str, msg: str) -> None:
class Registry:
_functions: typing.MutableMapping[str, FunctionInfo]
- _has_http_func: bool = False
def __init__(self) -> None:
self._functions = {}
@@ -57,8 +56,6 @@ def get_function(self, function_id: str) -> FunctionInfo:
return None
- def has_http_func(self) -> bool:
- return self._has_http_func
@staticmethod
def get_explicit_and_implicit_return(binding_name: str,
@@ -337,9 +334,6 @@ def add_func_to_registry_and_return_funcinfo(self, function,
self._functions[function_id] = function_info
- if not self._has_http_func:
- self._has_http_func = function_info.is_http_func
-
return function_info
def add_function(self, function_id: str,
diff --git a/azure_functions_worker/http_v2.py b/azure_functions_worker/http_v2.py
index 52e993735..5d00ecd6a 100644
--- a/azure_functions_worker/http_v2.py
+++ b/azure_functions_worker/http_v2.py
@@ -220,15 +220,7 @@ def http_v2_enabled(cls, functions=None, **kwargs):
# If not checked yet, mark as checked
cls._http_v2_enabled_checked = True
- # Check if there are functions provided and if any of them has
- # HTTP triggers
- cls._http_v2_enabled = functions is not None and \
- functions.has_http_func()
-
- # If HTTP functions are present, perform additional checks
- if cls._http_v2_enabled:
- # Check if HTTP/2 is enabled
- cls._http_v2_enabled = cls._check_http_v2_enabled()
+ cls._http_v2_enabled = cls._check_http_v2_enabled()
# Return the result of HTTP/2 enablement
return cls._http_v2_enabled
From 9221846ad42e45b79beedc0634350df539bf6053 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Wed, 10 Apr 2024 01:08:00 -0700
Subject: [PATCH 077/101] fix test
---
tests/endtoend/test_http_functions.py | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/tests/endtoend/test_http_functions.py b/tests/endtoend/test_http_functions.py
index f3a8a6e07..f6e6e3a76 100644
--- a/tests/endtoend/test_http_functions.py
+++ b/tests/endtoend/test_http_functions.py
@@ -243,9 +243,9 @@ def test_return_streaming(self):
streaming_url, timeout=REQUEST_TIMEOUT_SEC, stream=True)
self.assertTrue(r.ok)
# Validate streaming content
- expected_content = [b"First chunk\n", b"Second chunk\n"]
+ expected_content = [b'First', b' chun', b'k\nSec', b'ond c', b'hunk\n']
received_content = []
- for chunk in r.iter_content(chunk_size=1024):
+ for chunk in r.iter_content(chunk_size=5):
if chunk:
received_content.append(chunk)
self.assertEqual(received_content, expected_content)
From a70ca752940f4d9662c047fbbbc1881ab6feace4 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Wed, 10 Apr 2024 01:19:52 -0700
Subject: [PATCH 078/101] style
---
azure_functions_worker/functions.py | 1 -
1 file changed, 1 deletion(-)
diff --git a/azure_functions_worker/functions.py b/azure_functions_worker/functions.py
index 10bf70968..00296c441 100644
--- a/azure_functions_worker/functions.py
+++ b/azure_functions_worker/functions.py
@@ -56,7 +56,6 @@ def get_function(self, function_id: str) -> FunctionInfo:
return None
-
@staticmethod
def get_explicit_and_implicit_return(binding_name: str,
binding: BindingInfo,
From f7e136bdf473856a4635d481be3513219c3c3c1b Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Wed, 10 Apr 2024 10:14:37 -0700
Subject: [PATCH 079/101] revert cache ppl
---
.github/workflows/ci_consumption_workflow.yml | 15 +--------------
.github/workflows/ci_e2e_workflow.yml | 15 +--------------
.github/workflows/ci_ut_workflow.yml | 16 +---------------
3 files changed, 3 insertions(+), 43 deletions(-)
diff --git a/.github/workflows/ci_consumption_workflow.yml b/.github/workflows/ci_consumption_workflow.yml
index 34ef6eb51..14ee1b5e3 100644
--- a/.github/workflows/ci_consumption_workflow.yml
+++ b/.github/workflows/ci_consumption_workflow.yml
@@ -30,26 +30,13 @@ jobs:
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- - name: Get Date
- id: get-date
- run: |
- echo "todayDate=$(/bin/date -u "+%Y%m%d")" >> $GITHUB_ENV
- shell: bash
- - uses: actions/cache@v4
- id: cache-pip
- with:
- path: ${{ env.pythonLocation }}
- key: ${{ runner.os }}-pip-${{ hashFiles('setup.py') }}-${{ env.todayDate }}-${{ matrix.python-version }}
- - name: Install dependencies
- if: steps.cache-pip.outputs.cache-hit != 'true'
+ - name: Install dependencies and the worker
run: |
python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre
python -m pip install -U -e .[dev]
if [[ "${{ matrix.python-version }}" != "3.7" ]]; then
python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2]
fi
- - name: Install worker
- run: |
python setup.py build
- name: Running 3.7 Tests
if: matrix.python-version == 3.7
diff --git a/.github/workflows/ci_e2e_workflow.yml b/.github/workflows/ci_e2e_workflow.yml
index 55862f50d..58b69d131 100644
--- a/.github/workflows/ci_e2e_workflow.yml
+++ b/.github/workflows/ci_e2e_workflow.yml
@@ -39,18 +39,7 @@ jobs:
uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0.x"
- - name: Get Date
- id: get-date
- run: |
- echo "todayDate=$(/bin/date -u "+%Y%m%d")" >> $GITHUB_ENV
- shell: bash
- - uses: actions/cache@v4
- id: cache-pip
- with:
- path: ${{ env.pythonLocation }}
- key: ${{ runner.os }}-pip-${{ hashFiles('setup.py') }}-${{ env.todayDate }}-${{ matrix.python-version }}
- - name: Install dependencies
- if: steps.cache-pip.outputs.cache-hit != 'true'
+ - name: Install dependencies and the worker
run: |
python -m pip install --upgrade pip
python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre
@@ -59,8 +48,6 @@ jobs:
if [[ "${{ matrix.python-version }}" != "3.7" ]]; then
python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2]
fi
- - name: Install worker
- run: |
retry() {
local -r -i max_attempts="$1"; shift
local -r cmd="$@"
diff --git a/.github/workflows/ci_ut_workflow.yml b/.github/workflows/ci_ut_workflow.yml
index 01aa36070..8f17cdbb9 100644
--- a/.github/workflows/ci_ut_workflow.yml
+++ b/.github/workflows/ci_ut_workflow.yml
@@ -38,19 +38,7 @@ jobs:
uses: actions/setup-dotnet@v4
with:
dotnet-version: "8.0.x"
- - name: Get Date
- id: get-date
- run: |
- echo "todayDate=$(/bin/date -u "+%Y%m%d")" >> $GITHUB_ENV
- shell: bash
- - uses: actions/cache@v4
- id: cache-pip
- with:
- path: ${{ env.pythonLocation }}
- key: ${{ runner.os }}-pip-${{ hashFiles('setup.py') }}-${{ env.todayDate }}-${{ matrix.python-version }}
- - name: Install dependencies
- if: steps.cache-pip.outputs.cache-hit != 'true'
- run: |
+ - name: Install dependencies and the worker
python -m pip install --upgrade pip
python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre
python -m pip install -U -e .[dev]
@@ -58,8 +46,6 @@ jobs:
if [[ "${{ matrix.python-version }}" != "3.7" ]]; then
python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2]
fi
- - name: Install the worker
- run: |
retry() {
local -r -i max_attempts="$1"; shift
local -r cmd="$@"
From 16945e5fe80e6187ad79c5c65e0e8578fbe261e5 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Wed, 10 Apr 2024 10:19:20 -0700
Subject: [PATCH 080/101] revert ppl
---
.github/workflows/ci_consumption_workflow.yml | 4 ++--
.github/workflows/ci_e2e_workflow.yml | 16 ++++++++--------
.github/workflows/ci_ut_workflow.yml | 19 +++++++++----------
3 files changed, 19 insertions(+), 20 deletions(-)
diff --git a/.github/workflows/ci_consumption_workflow.yml b/.github/workflows/ci_consumption_workflow.yml
index 14ee1b5e3..48704ef3c 100644
--- a/.github/workflows/ci_consumption_workflow.yml
+++ b/.github/workflows/ci_consumption_workflow.yml
@@ -30,12 +30,12 @@ jobs:
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- - name: Install dependencies and the worker
+ - name: Install dependencies
run: |
python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre
python -m pip install -U -e .[dev]
if [[ "${{ matrix.python-version }}" != "3.7" ]]; then
- python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2]
+ python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2]
fi
python setup.py build
- name: Running 3.7 Tests
diff --git a/.github/workflows/ci_e2e_workflow.yml b/.github/workflows/ci_e2e_workflow.yml
index 58b69d131..3d795261b 100644
--- a/.github/workflows/ci_e2e_workflow.yml
+++ b/.github/workflows/ci_e2e_workflow.yml
@@ -13,7 +13,7 @@ on:
push:
branches: [dev, main, release/*]
pull_request:
- branches: [ dev, main, release/* ]
+ branches: [dev, main, release/*]
schedule:
# Monday to Thursday 3 AM CST build
# * is a special character in YAML so you have to quote this string
@@ -41,13 +41,6 @@ jobs:
dotnet-version: "8.0.x"
- name: Install dependencies and the worker
run: |
- python -m pip install --upgrade pip
- python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre
- python -m pip install -U -e .[dev]
- # Conditionally install test dependencies for Python 3.8 and later
- if [[ "${{ matrix.python-version }}" != "3.7" ]]; then
- python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2]
- fi
retry() {
local -r -i max_attempts="$1"; shift
local -r cmd="$@"
@@ -65,6 +58,13 @@ jobs:
done
}
+ python -m pip install --upgrade pip
+ python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre
+ python -m pip install -U -e .[dev]
+ if [[ "${{ matrix.python-version }}" != "3.7" ]]; then
+ python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2]
+ fi
+
# Retry a couple times to avoid certificate issue
retry 5 python setup.py build
retry 5 python setup.py webhost --branch-name=dev
diff --git a/.github/workflows/ci_ut_workflow.yml b/.github/workflows/ci_ut_workflow.yml
index 8f17cdbb9..0fd99d3a1 100644
--- a/.github/workflows/ci_ut_workflow.yml
+++ b/.github/workflows/ci_ut_workflow.yml
@@ -15,7 +15,6 @@ on:
# * is a special character in YAML so you have to quote this string
- cron: "0 8 * * 1,2,3,4"
push:
- branches: [ dev, main, release/* ]
pull_request:
branches: [ dev, main, release/* ]
@@ -39,13 +38,7 @@ jobs:
with:
dotnet-version: "8.0.x"
- name: Install dependencies and the worker
- python -m pip install --upgrade pip
- python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre
- python -m pip install -U -e .[dev]
- # Conditionally install test dependencies for Python 3.8 and later
- if [[ "${{ matrix.python-version }}" != "3.7" ]]; then
- python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2]
- fi
+ run: |
retry() {
local -r -i max_attempts="$1"; shift
local -r cmd="$@"
@@ -62,7 +55,14 @@ jobs:
fi
done
}
-
+
+ python -m pip install --upgrade pip
+ python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre
+ python -m pip install -U -e .[dev]
+ if [[ "${{ matrix.python-version }}" != "3.7" ]]; then
+ python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple --pre -U -e .[test-http-v2]
+ fi
+
# Retry a couple times to avoid certificate issue
retry 5 python setup.py build
retry 5 python setup.py webhost --branch-name=dev
@@ -88,4 +88,3 @@ jobs:
name: Test WebHost Logs ${{ github.run_id }} ${{ matrix.python-version }}
path: logs/*.log
if-no-files-found: ignore
-
From 0d9b5ca702960a8f87d9bb0165ffed9cd729684e Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Wed, 10 Apr 2024 11:02:43 -0700
Subject: [PATCH 081/101] pip fix
---
.github/workflows/ci_consumption_workflow.yml | 1 +
.github/workflows/ci_e2e_workflow.yml | 2 +-
.github/workflows/ci_ut_workflow.yml | 2 +-
3 files changed, 3 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/ci_consumption_workflow.yml b/.github/workflows/ci_consumption_workflow.yml
index 48704ef3c..c610347fe 100644
--- a/.github/workflows/ci_consumption_workflow.yml
+++ b/.github/workflows/ci_consumption_workflow.yml
@@ -32,6 +32,7 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
+ python -m pip install --upgrade pip==23.0
python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre
python -m pip install -U -e .[dev]
if [[ "${{ matrix.python-version }}" != "3.7" ]]; then
diff --git a/.github/workflows/ci_e2e_workflow.yml b/.github/workflows/ci_e2e_workflow.yml
index 3d795261b..5c90cfdb8 100644
--- a/.github/workflows/ci_e2e_workflow.yml
+++ b/.github/workflows/ci_e2e_workflow.yml
@@ -58,7 +58,7 @@ jobs:
done
}
- python -m pip install --upgrade pip
+ python -m pip install --upgrade pip==23.0
python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre
python -m pip install -U -e .[dev]
if [[ "${{ matrix.python-version }}" != "3.7" ]]; then
diff --git a/.github/workflows/ci_ut_workflow.yml b/.github/workflows/ci_ut_workflow.yml
index 0fd99d3a1..a24a0675e 100644
--- a/.github/workflows/ci_ut_workflow.yml
+++ b/.github/workflows/ci_ut_workflow.yml
@@ -56,7 +56,7 @@ jobs:
done
}
- python -m pip install --upgrade pip
+ python -m pip install --upgrade pip==23.0
python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre
python -m pip install -U -e .[dev]
if [[ "${{ matrix.python-version }}" != "3.7" ]]; then
From efbeff35db288a47783e2c5026e4b4e0e246a733 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Wed, 10 Apr 2024 12:39:20 -0700
Subject: [PATCH 082/101] try fix 3.7 ppl
---
setup.py | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/setup.py b/setup.py
index e82b5eb33..d2ff37724 100644
--- a/setup.py
+++ b/setup.py
@@ -109,7 +109,8 @@
"opencv-python",
"pandas",
"numpy",
- "pre-commit"
+ "pre-commit",
+ "importlib-metadata"
],
"test-http-v2": ["azure-functions-extension-fastapi", "ujson", "orjson"]
}
From b49fc06b72e4890bbc0bf2c9367b9035787cdbeb Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Wed, 10 Apr 2024 12:43:37 -0700
Subject: [PATCH 083/101] try
---
.github/workflows/ci_ut_workflow.yml | 1 +
setup.py | 3 +--
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/ci_ut_workflow.yml b/.github/workflows/ci_ut_workflow.yml
index a24a0675e..fda117d02 100644
--- a/.github/workflows/ci_ut_workflow.yml
+++ b/.github/workflows/ci_ut_workflow.yml
@@ -57,6 +57,7 @@ jobs:
}
python -m pip install --upgrade pip==23.0
+ python -m pip install importlib-metadata
python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre
python -m pip install -U -e .[dev]
if [[ "${{ matrix.python-version }}" != "3.7" ]]; then
diff --git a/setup.py b/setup.py
index d2ff37724..e82b5eb33 100644
--- a/setup.py
+++ b/setup.py
@@ -109,8 +109,7 @@
"opencv-python",
"pandas",
"numpy",
- "pre-commit",
- "importlib-metadata"
+ "pre-commit"
],
"test-http-v2": ["azure-functions-extension-fastapi", "ujson", "orjson"]
}
From 5b8af9e2113feeca11088132ea31095d211338ad Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Wed, 10 Apr 2024 12:51:13 -0700
Subject: [PATCH 084/101] oh meta
---
.github/workflows/ci_ut_workflow.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.github/workflows/ci_ut_workflow.yml b/.github/workflows/ci_ut_workflow.yml
index fda117d02..0dcb72106 100644
--- a/.github/workflows/ci_ut_workflow.yml
+++ b/.github/workflows/ci_ut_workflow.yml
@@ -57,7 +57,7 @@ jobs:
}
python -m pip install --upgrade pip==23.0
- python -m pip install importlib-metadata
+ python -m pip install importlib_metadata
python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre
python -m pip install -U -e .[dev]
if [[ "${{ matrix.python-version }}" != "3.7" ]]; then
From ae97beab07853b89a874bbd3d8f25a7ae23b34db Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Wed, 10 Apr 2024 13:07:37 -0700
Subject: [PATCH 085/101] fix
---
setup.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/setup.py b/setup.py
index e82b5eb33..d90438021 100644
--- a/setup.py
+++ b/setup.py
@@ -85,6 +85,7 @@
EXTRA_REQUIRES = {
"dev": [
+ "importlib_metadata",
"azure-eventhub~=5.7.0", # Used for EventHub E2E tests
"azure-functions-durable", # Used for Durable E2E tests
"flask",
From 6f01bcb9eaefb5b5eef5e450a4e7977ac4e12ae1 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Wed, 10 Apr 2024 13:39:46 -0700
Subject: [PATCH 086/101] fix
---
setup.py | 1 +
1 file changed, 1 insertion(+)
diff --git a/setup.py b/setup.py
index d90438021..8c78e8813 100644
--- a/setup.py
+++ b/setup.py
@@ -92,6 +92,7 @@
"fastapi~=0.85.0", # Used for ASGIMiddleware test
"pydantic",
"pycryptodome~=3.10.1",
+ "cmake==3.29.0.1"
"flake8~=4.0.1",
"mypy",
"pytest",
From 797f7167a3caad4783aa6e259f77d332dac132b3 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Wed, 10 Apr 2024 14:08:31 -0700
Subject: [PATCH 087/101] fix
---
.github/workflows/ci_ut_workflow.yml | 2 +-
setup.py | 2 --
2 files changed, 1 insertion(+), 3 deletions(-)
diff --git a/.github/workflows/ci_ut_workflow.yml b/.github/workflows/ci_ut_workflow.yml
index 0dcb72106..e86e32e65 100644
--- a/.github/workflows/ci_ut_workflow.yml
+++ b/.github/workflows/ci_ut_workflow.yml
@@ -57,7 +57,7 @@ jobs:
}
python -m pip install --upgrade pip==23.0
- python -m pip install importlib_metadata
+ python -m pip install pipdeptree
python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre
python -m pip install -U -e .[dev]
if [[ "${{ matrix.python-version }}" != "3.7" ]]; then
diff --git a/setup.py b/setup.py
index 8c78e8813..e82b5eb33 100644
--- a/setup.py
+++ b/setup.py
@@ -85,14 +85,12 @@
EXTRA_REQUIRES = {
"dev": [
- "importlib_metadata",
"azure-eventhub~=5.7.0", # Used for EventHub E2E tests
"azure-functions-durable", # Used for Durable E2E tests
"flask",
"fastapi~=0.85.0", # Used for ASGIMiddleware test
"pydantic",
"pycryptodome~=3.10.1",
- "cmake==3.29.0.1"
"flake8~=4.0.1",
"mypy",
"pytest",
From de0cc9830b44d4bd14e5cd8081589d153b4b62f9 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Wed, 10 Apr 2024 14:35:48 -0700
Subject: [PATCH 088/101] dont pin fastapi to fix cmake err
---
setup.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/setup.py b/setup.py
index e82b5eb33..6fde7ed5b 100644
--- a/setup.py
+++ b/setup.py
@@ -88,7 +88,7 @@
"azure-eventhub~=5.7.0", # Used for EventHub E2E tests
"azure-functions-durable", # Used for Durable E2E tests
"flask",
- "fastapi~=0.85.0", # Used for ASGIMiddleware test
+ "fastapi", # Used for ASGIMiddleware test
"pydantic",
"pycryptodome~=3.10.1",
"flake8~=4.0.1",
From e61734094b4bd8427caf63e686eb7691fe9f557a Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Wed, 10 Apr 2024 14:40:15 -0700
Subject: [PATCH 089/101] fix
---
setup.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/setup.py b/setup.py
index 6fde7ed5b..b55f460a0 100644
--- a/setup.py
+++ b/setup.py
@@ -85,7 +85,7 @@
EXTRA_REQUIRES = {
"dev": [
- "azure-eventhub~=5.7.0", # Used for EventHub E2E tests
+ "azure-eventhub", # Used for EventHub E2E tests
"azure-functions-durable", # Used for Durable E2E tests
"flask",
"fastapi", # Used for ASGIMiddleware test
From e28d76925c813e17f7a9449d309a4c900005b9ae Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Wed, 10 Apr 2024 14:48:13 -0700
Subject: [PATCH 090/101] fix
---
setup.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/setup.py b/setup.py
index b55f460a0..80afab519 100644
--- a/setup.py
+++ b/setup.py
@@ -88,7 +88,7 @@
"azure-eventhub", # Used for EventHub E2E tests
"azure-functions-durable", # Used for Durable E2E tests
"flask",
- "fastapi", # Used for ASGIMiddleware test
+ "fastapi~=0.85.0", # Used for ASGIMiddleware test
"pydantic",
"pycryptodome~=3.10.1",
"flake8~=4.0.1",
From 7449d0db5349820a29ef029f9f0624c22995131c Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Wed, 10 Apr 2024 19:18:13 -0700
Subject: [PATCH 091/101] update azfunc base
---
azure_functions_worker/http_v2.py | 2 +-
setup.py | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/azure_functions_worker/http_v2.py b/azure_functions_worker/http_v2.py
index 5d00ecd6a..8c20d8b32 100644
--- a/azure_functions_worker/http_v2.py
+++ b/azure_functions_worker/http_v2.py
@@ -235,7 +235,7 @@ def _check_http_v2_enabled(cls):
is_envvar_false(PYTHON_ENABLE_INIT_INDEXING):
return False
- import azure.functions.extension.base as ext_base
+ import azurefunctions.extensions.base as ext_base
cls._ext_base = ext_base
return cls._ext_base.HttpV2FeatureChecker.http_v2_enabled()
diff --git a/setup.py b/setup.py
index 80afab519..7b518151f 100644
--- a/setup.py
+++ b/setup.py
@@ -80,7 +80,7 @@
else:
INSTALL_REQUIRES.extend(
("protobuf~=4.22.0", "grpcio-tools~=1.54.2", "grpcio~=1.54.2",
- "azure-functions-extension-base")
+ "azurefunctions-extensions-base")
)
EXTRA_REQUIRES = {
From 450b43e5fc2196698ba4a7f05c6335c4e3302f77 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Wed, 10 Apr 2024 19:47:07 -0700
Subject: [PATCH 092/101] update
---
setup.py | 6 +++++-
.../http_functions_v2/fastapi/file_name/main.py | 2 +-
.../http_functions_v2/fastapi/function_app.py | 2 +-
.../dispatcher_functions/http_v2/fastapi/function_app.py | 2 +-
.../http_v2_functions/fastapi/function_app.py | 2 +-
5 files changed, 9 insertions(+), 5 deletions(-)
diff --git a/setup.py b/setup.py
index 7b518151f..85d6941c9 100644
--- a/setup.py
+++ b/setup.py
@@ -111,7 +111,11 @@
"numpy",
"pre-commit"
],
- "test-http-v2": ["azure-functions-extension-fastapi", "ujson", "orjson"]
+ "test-http-v2": [
+ "azurefunctions-extensions-http-fastapi",
+ "ujson",
+ "orjson"
+ ]
}
diff --git a/tests/endtoend/http_functions/http_functions_v2/fastapi/file_name/main.py b/tests/endtoend/http_functions/http_functions_v2/fastapi/file_name/main.py
index c9718fef5..65ae5525b 100644
--- a/tests/endtoend/http_functions/http_functions_v2/fastapi/file_name/main.py
+++ b/tests/endtoend/http_functions/http_functions_v2/fastapi/file_name/main.py
@@ -7,7 +7,7 @@
import azure.functions as func
-from azure.functions.extension.fastapi import Request, Response
+from azurefunctions.extensions.http.fastapi import Request, Response
app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
diff --git a/tests/endtoend/http_functions/http_functions_v2/fastapi/function_app.py b/tests/endtoend/http_functions/http_functions_v2/fastapi/function_app.py
index b82e0baee..355de98a8 100644
--- a/tests/endtoend/http_functions/http_functions_v2/fastapi/function_app.py
+++ b/tests/endtoend/http_functions/http_functions_v2/fastapi/function_app.py
@@ -5,7 +5,7 @@
import time
import azure.functions as func
-from azure.functions.extension.fastapi import Request, Response, \
+from azurefunctions.extensions.http.fastapi import Request, Response, \
StreamingResponse, HTMLResponse, \
UJSONResponse, ORJSONResponse, FileResponse
diff --git a/tests/unittests/dispatcher_functions/http_v2/fastapi/function_app.py b/tests/unittests/dispatcher_functions/http_v2/fastapi/function_app.py
index f202890de..a2c15d419 100644
--- a/tests/unittests/dispatcher_functions/http_v2/fastapi/function_app.py
+++ b/tests/unittests/dispatcher_functions/http_v2/fastapi/function_app.py
@@ -1,4 +1,4 @@
-from azure.functions.extension.fastapi import Request, Response
+from azurefunctions.extensions.http.fastapi import Request, Response
import azure.functions as func
diff --git a/tests/unittests/http_functions/http_v2_functions/fastapi/function_app.py b/tests/unittests/http_functions/http_v2_functions/fastapi/function_app.py
index 9830f572e..b4018466d 100644
--- a/tests/unittests/http_functions/http_v2_functions/fastapi/function_app.py
+++ b/tests/unittests/http_functions/http_v2_functions/fastapi/function_app.py
@@ -6,7 +6,7 @@
import sys
import time
from urllib.request import urlopen
-from azure.functions.extension.fastapi import Request, Response, \
+from azurefunctions.extensions.http.fastapi import Request, Response, \
HTMLResponse, RedirectResponse
import azure.functions as func
from pydantic import BaseModel
From 3ab30f79e9c97ec20d4637975852c0c76eb592d1 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Thu, 11 Apr 2024 10:18:30 -0700
Subject: [PATCH 093/101] feedback
---
azure_functions_worker/dispatcher.py | 11 +++--------
azure_functions_worker/functions.py | 14 +++++++++-----
azure_functions_worker/http_v2.py | 10 ++++++++++
3 files changed, 22 insertions(+), 13 deletions(-)
diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py
index 65c9ebd57..d31c10b2f 100644
--- a/azure_functions_worker/dispatcher.py
+++ b/azure_functions_worker/dispatcher.py
@@ -32,7 +32,8 @@
PYTHON_LANGUAGE_RUNTIME, PYTHON_ENABLE_INIT_INDEXING,
METADATA_PROPERTIES_WORKER_INDEXED)
from .extension import ExtensionManager
-from .http_v2 import http_coordinator, initialize_http_server, HttpV2Registry
+from .http_v2 import http_coordinator, initialize_http_server, HttpV2Registry, \
+ sync_http_request
from .logging import disable_console_logging, enable_console_logging
from .logging import (logger, error_logger, is_system_log_category,
CONSOLE_LOG_PREFIX, format_exception)
@@ -549,13 +550,7 @@ async def _handle__invocation_request(self, request):
http_request = await http_coordinator.get_http_request_async(
invocation_id)
- route_params = {key: item.string for key, item
- in invoc_request.trigger_metadata.items()
- if key not in ['Headers', 'Query']}
-
- (HttpV2Registry.ext_base().RequestTrackerMeta
- .get_synchronizer()
- .sync_route_params(http_request, route_params))
+ await sync_http_request(http_request, invoc_request)
args[fi.trigger_metadata.get('param_name')] = http_request
fi_context = self._get_context(invoc_request, fi.name,
diff --git a/azure_functions_worker/functions.py b/azure_functions_worker/functions.py
index 00296c441..e529f37ee 100644
--- a/azure_functions_worker/functions.py
+++ b/azure_functions_worker/functions.py
@@ -302,11 +302,7 @@ def add_func_to_registry_and_return_funcinfo(self, function,
str, ParamTypeInfo],
return_type: str):
- http_trigger_param_name = next(
- (input_type for input_type, type_info in input_types.items()
- if type_info.binding_name == HTTP_TRIGGER),
- None
- )
+ http_trigger_param_name = self._get_http_trigger_param_name(input_types)
trigger_metadata = None
is_http_func = False
@@ -335,6 +331,14 @@ def add_func_to_registry_and_return_funcinfo(self, function,
return function_info
+ def _get_http_trigger_param_name(self, input_types):
+ http_trigger_param_name = next(
+ (input_type for input_type, type_info in input_types.items()
+ if type_info.binding_name == HTTP_TRIGGER),
+ None
+ )
+ return http_trigger_param_name
+
def add_function(self, function_id: str,
func: typing.Callable,
metadata: protos.RpcFunctionMetadata):
diff --git a/azure_functions_worker/http_v2.py b/azure_functions_worker/http_v2.py
index 8c20d8b32..d75548922 100644
--- a/azure_functions_worker/http_v2.py
+++ b/azure_functions_worker/http_v2.py
@@ -208,6 +208,16 @@ async def catch_all(request: request_type): # type: ignore
return web_server_address
+async def sync_http_request(http_request, invoc_request):
+ # Sync http request route params from invoc_request to http_request
+ route_params = {key: item.string for key, item
+ in invoc_request.trigger_metadata.items()
+ if key not in ['Headers', 'Query']}
+ (HttpV2Registry.ext_base().RequestTrackerMeta
+ .get_synchronizer()
+ .sync_route_params(http_request, route_params))
+
+
class HttpV2Registry:
_http_v2_enabled = False
_ext_base = None
From e85ea9e2fe6f93121008ccf7d5c94df3134c5b05 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Thu, 11 Apr 2024 10:41:51 -0700
Subject: [PATCH 094/101] feedback
---
.github/workflows/ci_consumption_workflow.yml | 2 +-
.github/workflows/ci_e2e_workflow.yml | 2 +-
.github/workflows/ci_ut_workflow.yml | 3 +--
azure_functions_worker/dispatcher.py | 4 ++--
azure_functions_worker/http_v2.py | 13 +++++++------
5 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/.github/workflows/ci_consumption_workflow.yml b/.github/workflows/ci_consumption_workflow.yml
index c610347fe..3a280cdb1 100644
--- a/.github/workflows/ci_consumption_workflow.yml
+++ b/.github/workflows/ci_consumption_workflow.yml
@@ -32,7 +32,7 @@ jobs:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
- python -m pip install --upgrade pip==23.0
+ python -m pip install --upgrade pip
python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre
python -m pip install -U -e .[dev]
if [[ "${{ matrix.python-version }}" != "3.7" ]]; then
diff --git a/.github/workflows/ci_e2e_workflow.yml b/.github/workflows/ci_e2e_workflow.yml
index 5c90cfdb8..3d795261b 100644
--- a/.github/workflows/ci_e2e_workflow.yml
+++ b/.github/workflows/ci_e2e_workflow.yml
@@ -58,7 +58,7 @@ jobs:
done
}
- python -m pip install --upgrade pip==23.0
+ python -m pip install --upgrade pip
python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre
python -m pip install -U -e .[dev]
if [[ "${{ matrix.python-version }}" != "3.7" ]]; then
diff --git a/.github/workflows/ci_ut_workflow.yml b/.github/workflows/ci_ut_workflow.yml
index e86e32e65..0fd99d3a1 100644
--- a/.github/workflows/ci_ut_workflow.yml
+++ b/.github/workflows/ci_ut_workflow.yml
@@ -56,8 +56,7 @@ jobs:
done
}
- python -m pip install --upgrade pip==23.0
- python -m pip install pipdeptree
+ python -m pip install --upgrade pip
python -m pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple -U azure-functions --pre
python -m pip install -U -e .[dev]
if [[ "${{ matrix.python-version }}" != "3.7" ]]; then
diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py
index d31c10b2f..a981fe049 100644
--- a/azure_functions_worker/dispatcher.py
+++ b/azure_functions_worker/dispatcher.py
@@ -314,7 +314,7 @@ async def _handle__worker_init_request(self, request):
self._function_metadata_exception = ex
try:
- if HttpV2Registry.http_v2_enabled(self._functions):
+ if HttpV2Registry.http_v2_enabled():
capabilities[constants.HTTP_URI] = \
initialize_http_server(self._host)
except Exception as ex:
@@ -689,7 +689,7 @@ async def _handle__function_environment_reload_request(self, request):
except Exception as ex:
self._function_metadata_exception = ex
- if HttpV2Registry.http_v2_enabled(self._functions):
+ if HttpV2Registry.http_v2_enabled():
capabilities[constants.HTTP_URI] = \
initialize_http_server(self._host)
diff --git a/azure_functions_worker/http_v2.py b/azure_functions_worker/http_v2.py
index d75548922..b95a60afd 100644
--- a/azure_functions_worker/http_v2.py
+++ b/azure_functions_worker/http_v2.py
@@ -143,7 +143,7 @@ def _pop_http_request(self, invoc_id):
context_ref.http_request = None
return request
- raise Exception(f"No http request found for invocation {invoc_id}")
+ raise ValueError(f"No http request found for invocation {invoc_id}")
def _pop_http_response(self, invoc_id):
context_ref = self._context_references.get(invoc_id)
@@ -151,7 +151,8 @@ def _pop_http_response(self, invoc_id):
if response is not None:
context_ref.http_response = None
return response
- raise Exception(f"No http response found for invocation {invoc_id}")
+
+ raise ValueError(f"No http response found for invocation {invoc_id}")
def get_unused_tcp_port():
@@ -184,12 +185,12 @@ async def catch_all(request: request_type): # type: ignore
invoc_id = request.headers.get(X_MS_INVOCATION_ID)
if invoc_id is None:
raise ValueError(f"Header {X_MS_INVOCATION_ID} not found")
- logger.info(f'Received HTTP request for invocation {invoc_id}')
+ logger.info('Received HTTP request for invocation %s', invoc_id)
http_coordinator.set_http_request(invoc_id, request)
http_resp = \
await http_coordinator.await_http_response_async(invoc_id)
- logger.info(f'Sending HTTP response for invocation {invoc_id}')
+ logger.info('Sending HTTP response for invocation %s', invoc_id)
# if http_resp is an python exception, raise it
if isinstance(http_resp, Exception):
raise http_resp
@@ -203,7 +204,7 @@ async def catch_all(request: request_type): # type: ignore
loop.create_task(web_server_run_task)
web_server_address = f"http://{host_addr}:{unused_port}"
- logger.info(f'HTTP server starting on {web_server_address}')
+ logger.info('HTTP server starting on %s', web_server_address)
return web_server_address
@@ -224,7 +225,7 @@ class HttpV2Registry:
_http_v2_enabled_checked = False
@classmethod
- def http_v2_enabled(cls, functions=None, **kwargs):
+ def http_v2_enabled(cls, **kwargs):
# Check if HTTP/2 enablement has already been checked
if not cls._http_v2_enabled_checked:
# If not checked yet, mark as checked
From a695ba3fce92c4e16a181d8eabacdeb9ca6fb1c2 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Thu, 11 Apr 2024 11:29:55 -0700
Subject: [PATCH 095/101] feedback
---
azure_functions_worker/dispatcher.py | 33 ++++------
azure_functions_worker/exceptions.py | 9 +++
azure_functions_worker/http_v2.py | 99 +++++++++++++++-------------
3 files changed, 75 insertions(+), 66 deletions(-)
create mode 100644 azure_functions_worker/exceptions.py
diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py
index a981fe049..4a7c1b5c1 100644
--- a/azure_functions_worker/dispatcher.py
+++ b/azure_functions_worker/dispatcher.py
@@ -31,6 +31,7 @@
PYTHON_SCRIPT_FILE_NAME_DEFAULT,
PYTHON_LANGUAGE_RUNTIME, PYTHON_ENABLE_INIT_INDEXING,
METADATA_PROPERTIES_WORKER_INDEXED)
+from .exceptions import HttpServerInitError
from .extension import ExtensionManager
from .http_v2 import http_coordinator, initialize_http_server, HttpV2Registry, \
sync_http_request
@@ -112,8 +113,7 @@ def get_sync_tp_workers_set(self):
3.9 scenarios (as we'll start passing only None by default), and we
need to get that information.
- Ref: concurrent.futures.thread.ThreadPoolExecutor.__init__
- ._max_workers
+ Ref: concurrent.futures.thread.ThreadPoolExecutor.__init__._max_workers
"""
return self._sync_call_tp._max_workers
@@ -310,22 +310,15 @@ async def _handle__worker_init_request(self, request):
self.load_function_metadata(
worker_init_request.function_app_directory,
caller_info="worker_init_request")
- except Exception as ex:
- self._function_metadata_exception = ex
- try:
if HttpV2Registry.http_v2_enabled():
capabilities[constants.HTTP_URI] = \
initialize_http_server(self._host)
+
+ except HttpServerInitError:
+ raise
except Exception as ex:
- return protos.StreamingMessage(
- request_id=self.request_id,
- worker_init_response=protos.WorkerInitResponse(
- result=protos.StatusResult(
- status=protos.StatusResult.Failure,
- exception=self._serialize_exception(ex))
- )
- )
+ self._function_metadata_exception = ex
return protos.StreamingMessage(
request_id=self.request_id,
@@ -333,9 +326,7 @@ async def _handle__worker_init_request(self, request):
capabilities=capabilities,
worker_metadata=self.get_worker_metadata(),
result=protos.StatusResult(
- status=protos.StatusResult.Success),
- ),
- )
+ status=protos.StatusResult.Success)))
async def _handle__worker_status_request(self, request):
# Logging is not necessary in this request since the response is used
@@ -686,13 +677,15 @@ async def _handle__function_environment_reload_request(self, request):
self.load_function_metadata(
directory,
caller_info="environment_reload_request")
+
+ if HttpV2Registry.http_v2_enabled():
+ capabilities[constants.HTTP_URI] = \
+ initialize_http_server(self._host)
+ except HttpServerInitError:
+ raise
except Exception as ex:
self._function_metadata_exception = ex
- if HttpV2Registry.http_v2_enabled():
- capabilities[constants.HTTP_URI] = \
- initialize_http_server(self._host)
-
# Change function app directory
if getattr(func_env_reload_request,
'function_app_directory', None):
diff --git a/azure_functions_worker/exceptions.py b/azure_functions_worker/exceptions.py
new file mode 100644
index 000000000..910d0ab73
--- /dev/null
+++ b/azure_functions_worker/exceptions.py
@@ -0,0 +1,9 @@
+# http v2 exception types
+class HttpServerInitError(Exception):
+ """Exception raised when there is an error during HTTP server
+ initialization."""
+
+
+class MissingHeaderError(ValueError):
+ """Exception raised when a required header is missing in the
+ HTTP request."""
diff --git a/azure_functions_worker/http_v2.py b/azure_functions_worker/http_v2.py
index b95a60afd..93262b430 100644
--- a/azure_functions_worker/http_v2.py
+++ b/azure_functions_worker/http_v2.py
@@ -7,6 +7,8 @@
from azure_functions_worker.constants import X_MS_INVOCATION_ID, \
BASE_EXT_SUPPORTED_PY_MINOR_VERSION, PYTHON_ENABLE_INIT_INDEXING
+from azure_functions_worker.exceptions import MissingHeaderError, \
+ HttpServerInitError
from azure_functions_worker.logging import logger
from azure_functions_worker.utils.common import is_envvar_false
@@ -113,8 +115,8 @@ def set_http_request(self, invoc_id, http_request):
def set_http_response(self, invoc_id, http_response):
if invoc_id not in self._context_references:
- raise Exception("No context reference found for invocation "
- f"{invoc_id}")
+ raise KeyError("No context reference found for invocation %s"
+ % invoc_id)
context_ref = self._context_references.get(invoc_id)
context_ref.http_response = http_response
@@ -122,16 +124,15 @@ async def get_http_request_async(self, invoc_id):
if invoc_id not in self._context_references:
self._context_references[invoc_id] = AsyncContextReference()
- await asyncio.sleep(0)
await self._context_references.get(
invoc_id).http_request_available_event.wait()
return self._pop_http_request(invoc_id)
async def await_http_response_async(self, invoc_id):
if invoc_id not in self._context_references:
- raise Exception("No context reference found for invocation "
- f"{invoc_id}")
- await asyncio.sleep(0)
+ raise KeyError("No context reference found for invocation %s"
+ % invoc_id)
+
await self._context_references.get(
invoc_id).http_response_available_event.wait()
return self._pop_http_response(invoc_id)
@@ -143,7 +144,7 @@ def _pop_http_request(self, invoc_id):
context_ref.http_request = None
return request
- raise ValueError(f"No http request found for invocation {invoc_id}")
+ raise ValueError("No http request found for invocation %s" % invoc_id)
def _pop_http_response(self, invoc_id):
context_ref = self._context_references.get(invoc_id)
@@ -152,7 +153,7 @@ def _pop_http_response(self, invoc_id):
context_ref.http_response = None
return response
- raise ValueError(f"No http response found for invocation {invoc_id}")
+ raise ValueError("No http response found for invocation %s" % invoc_id)
def get_unused_tcp_port():
@@ -169,44 +170,50 @@ def get_unused_tcp_port():
def initialize_http_server(host_addr, **kwargs):
- ext_base = HttpV2Registry.ext_base()
- web_extension_mod_name = ext_base.ModuleTrackerMeta.get_module()
- extension_module = importlib.import_module(web_extension_mod_name)
- web_app_class = extension_module.WebApp
- web_server_class = extension_module.WebServer
-
- unused_port = get_unused_tcp_port()
-
- app = web_app_class()
- request_type = ext_base.RequestTrackerMeta.get_request_type()
-
- @app.route
- async def catch_all(request: request_type): # type: ignore
- invoc_id = request.headers.get(X_MS_INVOCATION_ID)
- if invoc_id is None:
- raise ValueError(f"Header {X_MS_INVOCATION_ID} not found")
- logger.info('Received HTTP request for invocation %s', invoc_id)
- http_coordinator.set_http_request(invoc_id, request)
- http_resp = \
- await http_coordinator.await_http_response_async(invoc_id)
-
- logger.info('Sending HTTP response for invocation %s', invoc_id)
- # if http_resp is an python exception, raise it
- if isinstance(http_resp, Exception):
- raise http_resp
-
- return http_resp
-
- web_server = web_server_class(host_addr, unused_port, app)
- web_server_run_task = web_server.serve()
-
- loop = asyncio.get_event_loop()
- loop.create_task(web_server_run_task)
-
- web_server_address = f"http://{host_addr}:{unused_port}"
- logger.info('HTTP server starting on %s', web_server_address)
-
- return web_server_address
+ try:
+ ext_base = HttpV2Registry.ext_base()
+ web_extension_mod_name = ext_base.ModuleTrackerMeta.get_module()
+ extension_module = importlib.import_module(web_extension_mod_name)
+ web_app_class = extension_module.WebApp
+ web_server_class = extension_module.WebServer
+
+ unused_port = get_unused_tcp_port()
+
+ app = web_app_class()
+ request_type = ext_base.RequestTrackerMeta.get_request_type()
+
+ @app.route
+ async def catch_all(request: request_type): # type: ignore
+ invoc_id = request.headers.get(X_MS_INVOCATION_ID)
+ if invoc_id is None:
+ raise MissingHeaderError("Header %s not found" %
+ X_MS_INVOCATION_ID)
+ logger.info('Received HTTP request for invocation %s', invoc_id)
+ http_coordinator.set_http_request(invoc_id, request)
+ http_resp = \
+ await http_coordinator.await_http_response_async(invoc_id)
+
+ logger.info('Sending HTTP response for invocation %s', invoc_id)
+ # if http_resp is an python exception, raise it
+ if isinstance(http_resp, Exception):
+ raise http_resp
+
+ return http_resp
+
+ web_server = web_server_class(host_addr, unused_port, app)
+ web_server_run_task = web_server.serve()
+
+ loop = asyncio.get_event_loop()
+ loop.create_task(web_server_run_task)
+
+ web_server_address = f"http://{host_addr}:{unused_port}"
+ logger.info('HTTP server starting on %s', web_server_address)
+
+ return web_server_address
+
+ except Exception as e:
+ raise HttpServerInitError("Error initializing HTTP server: %s" % e) \
+ from e
async def sync_http_request(http_request, invoc_request):
From 62d7baceba19312d6615f7cbe8abc9d007fb42d3 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Thu, 11 Apr 2024 11:53:30 -0700
Subject: [PATCH 096/101] add docs
---
azure_functions_worker/http_v2.py | 18 ++++++++++++++++++
tests/unittests/test_http_v2.py | 4 ++--
2 files changed, 20 insertions(+), 2 deletions(-)
diff --git a/azure_functions_worker/http_v2.py b/azure_functions_worker/http_v2.py
index 93262b430..9892c6a9a 100644
--- a/azure_functions_worker/http_v2.py
+++ b/azure_functions_worker/http_v2.py
@@ -14,6 +14,9 @@
class BaseContextReference(abc.ABC):
+ """
+ Base class for context references.
+ """
def __init__(self, event_class, http_request=None, http_response=None,
function=None, fi_context=None, args=None,
http_trigger_param_name=None):
@@ -86,6 +89,9 @@ def http_response_available_event(self):
class AsyncContextReference(BaseContextReference):
+ """
+ Asynchronous context reference class.
+ """
def __init__(self, http_request=None, http_response=None, function=None,
fi_context=None, args=None):
super().__init__(event_class=asyncio.Event, http_request=http_request,
@@ -95,6 +101,9 @@ def __init__(self, http_request=None, http_response=None, function=None,
class SingletonMeta(type):
+ """
+ Metaclass for implementing the singleton pattern.
+ """
_instances = {}
def __call__(cls, *args, **kwargs):
@@ -104,6 +113,9 @@ def __call__(cls, *args, **kwargs):
class HttpCoordinator(metaclass=SingletonMeta):
+ """
+ HTTP coordinator class for managing HTTP v2 requests and responses.
+ """
def __init__(self):
self._context_references: Dict[str, BaseContextReference] = {}
@@ -170,6 +182,9 @@ def get_unused_tcp_port():
def initialize_http_server(host_addr, **kwargs):
+ """
+ Initialize HTTP v2 server for handling HTTP requests.
+ """
try:
ext_base = HttpV2Registry.ext_base()
web_extension_mod_name = ext_base.ModuleTrackerMeta.get_module()
@@ -227,6 +242,9 @@ async def sync_http_request(http_request, invoc_request):
class HttpV2Registry:
+ """
+ HTTP v2 registry class for managing HTTP v2 states.
+ """
_http_v2_enabled = False
_ext_base = None
_http_v2_enabled_checked = False
diff --git a/tests/unittests/test_http_v2.py b/tests/unittests/test_http_v2.py
index 1ab7af5e7..90d4d9b46 100644
--- a/tests/unittests/test_http_v2.py
+++ b/tests/unittests/test_http_v2.py
@@ -105,8 +105,8 @@ def test_await_http_response_async_invalid_invocation(self):
self.loop.run_until_complete(
http_coordinator.await_http_response_async(invalid_invoc_id))
self.assertEqual(str(context.exception),
- f"No context reference found for invocation "
- f"{invalid_invoc_id}")
+ f"'No context reference found for invocation "
+ f"{invalid_invoc_id}'")
def test_await_http_response_async_response_not_set(self):
invoc_id = "invocation_with_no_response"
From 142b5cecd3861e0feebefa820522d640c2ef1bd5 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Thu, 11 Apr 2024 17:13:03 -0700
Subject: [PATCH 097/101] merge fix
---
azure_functions_worker/bindings/meta.py | 10 +++++++---
1 file changed, 7 insertions(+), 3 deletions(-)
diff --git a/azure_functions_worker/bindings/meta.py b/azure_functions_worker/bindings/meta.py
index 2571f9a8e..4202b87a7 100644
--- a/azure_functions_worker/bindings/meta.py
+++ b/azure_functions_worker/bindings/meta.py
@@ -27,7 +27,8 @@
deferred_bindings_cache = {}
-def _check_http_input_type_annotation(bind_name: str, pytype: type, is_deferred_binding: bool) -> bool:
+def _check_http_input_type_annotation(bind_name: str, pytype: type,
+ is_deferred_binding: bool) -> bool:
if HttpV2Registry.http_v2_enabled():
return HttpV2Registry.ext_base().RequestTrackerMeta \
.check_type(pytype)
@@ -115,10 +116,13 @@ def is_trigger_binding(bind_name: str) -> bool:
return binding.has_trigger_support()
-def check_input_type_annotation(bind_name: str, pytype: type) -> bool:
+def check_input_type_annotation(bind_name: str,
+ pytype: type,
+ is_deferred_binding: bool) -> bool:
global INPUT_TYPE_CHECK_OVERRIDE_MAP
if bind_name in INPUT_TYPE_CHECK_OVERRIDE_MAP:
- return INPUT_TYPE_CHECK_OVERRIDE_MAP[bind_name](bind_name, pytype, is_deferred_binding)
+ return INPUT_TYPE_CHECK_OVERRIDE_MAP[bind_name](bind_name, pytype,
+ is_deferred_binding)
binding = get_binding(bind_name, is_deferred_binding)
From 3608c23da90d0793023e791e2acc4e1bccba7111 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Fri, 12 Apr 2024 10:45:01 -0700
Subject: [PATCH 098/101] feedback
---
azure_functions_worker/bindings/meta.py | 3 +--
1 file changed, 1 insertion(+), 2 deletions(-)
diff --git a/azure_functions_worker/bindings/meta.py b/azure_functions_worker/bindings/meta.py
index 4202b87a7..c1ff6e2ee 100644
--- a/azure_functions_worker/bindings/meta.py
+++ b/azure_functions_worker/bindings/meta.py
@@ -4,7 +4,6 @@
import sys
import typing
-from azure_functions_worker.constants import HTTP, HTTP_TRIGGER
from .. import protos
from . import datumdef
@@ -12,7 +11,7 @@
from .shared_memory_data_transfer import SharedMemoryManager
from ..http_v2 import HttpV2Registry
-from ..constants import CUSTOMER_PACKAGES_PATH
+from ..constants import CUSTOMER_PACKAGES_PATH, HTTP, HTTP_TRIGGER
from ..logging import logger
From a20ff5c2af102be4075ddedacc4f6029bf8e9a06 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Fri, 12 Apr 2024 11:56:05 -0700
Subject: [PATCH 099/101] feedback
---
azure_functions_worker/dispatcher.py | 3 +--
azure_functions_worker/exceptions.py | 9 ---------
azure_functions_worker/http_v2.py | 16 ++++++++++++++--
.../consumption_tests/test_linux_consumption.py | 3 ++-
.../http_functions_v2/fastapi/file_name/main.py | 3 ---
5 files changed, 17 insertions(+), 17 deletions(-)
delete mode 100644 azure_functions_worker/exceptions.py
diff --git a/azure_functions_worker/dispatcher.py b/azure_functions_worker/dispatcher.py
index 5c0ec106f..6938eb917 100644
--- a/azure_functions_worker/dispatcher.py
+++ b/azure_functions_worker/dispatcher.py
@@ -31,10 +31,9 @@
PYTHON_SCRIPT_FILE_NAME_DEFAULT,
PYTHON_LANGUAGE_RUNTIME, PYTHON_ENABLE_INIT_INDEXING,
METADATA_PROPERTIES_WORKER_INDEXED)
-from .exceptions import HttpServerInitError
from .extension import ExtensionManager
from .http_v2 import http_coordinator, initialize_http_server, HttpV2Registry, \
- sync_http_request
+ sync_http_request, HttpServerInitError
from .logging import disable_console_logging, enable_console_logging
from .logging import (logger, error_logger, is_system_log_category,
CONSOLE_LOG_PREFIX, format_exception)
diff --git a/azure_functions_worker/exceptions.py b/azure_functions_worker/exceptions.py
deleted file mode 100644
index 910d0ab73..000000000
--- a/azure_functions_worker/exceptions.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# http v2 exception types
-class HttpServerInitError(Exception):
- """Exception raised when there is an error during HTTP server
- initialization."""
-
-
-class MissingHeaderError(ValueError):
- """Exception raised when a required header is missing in the
- HTTP request."""
diff --git a/azure_functions_worker/http_v2.py b/azure_functions_worker/http_v2.py
index 9892c6a9a..d7fff59b6 100644
--- a/azure_functions_worker/http_v2.py
+++ b/azure_functions_worker/http_v2.py
@@ -1,3 +1,6 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
import abc
import asyncio
import importlib
@@ -7,12 +10,21 @@
from azure_functions_worker.constants import X_MS_INVOCATION_ID, \
BASE_EXT_SUPPORTED_PY_MINOR_VERSION, PYTHON_ENABLE_INIT_INDEXING
-from azure_functions_worker.exceptions import MissingHeaderError, \
- HttpServerInitError
from azure_functions_worker.logging import logger
from azure_functions_worker.utils.common import is_envvar_false
+# Http V2 Exceptions
+class HttpServerInitError(Exception):
+ """Exception raised when there is an error during HTTP server
+ initialization."""
+
+
+class MissingHeaderError(ValueError):
+ """Exception raised when a required header is missing in the
+ HTTP request."""
+
+
class BaseContextReference(abc.ABC):
"""
Base class for context references.
diff --git a/tests/consumption_tests/test_linux_consumption.py b/tests/consumption_tests/test_linux_consumption.py
index 9c5be090f..fbfd3bcea 100644
--- a/tests/consumption_tests/test_linux_consumption.py
+++ b/tests/consumption_tests/test_linux_consumption.py
@@ -340,7 +340,8 @@ def test_reload_variables_after_oom_error(self):
"This is testing only for python310")
def test_http_v2_fastapi_streaming_upload_download(self):
"""
- A function app with init indexing enabled
+ A function app using http v2 fastapi extension with streaming upload and
+ download
"""
with LinuxConsumptionWebHostController(_DEFAULT_HOST_VERSION,
self._py_version) as ctrl:
diff --git a/tests/endtoend/http_functions/http_functions_v2/fastapi/file_name/main.py b/tests/endtoend/http_functions/http_functions_v2/fastapi/file_name/main.py
index 65ae5525b..b149c1f78 100644
--- a/tests/endtoend/http_functions/http_functions_v2/fastapi/file_name/main.py
+++ b/tests/endtoend/http_functions/http_functions_v2/fastapi/file_name/main.py
@@ -1,6 +1,3 @@
-# Copyright (c) Microsoft Corporation. All rights reserved.
-# Licensed under the MIT License.
-
from datetime import datetime
import logging
import time
From 15ba6b44977ab62c70cb5cc7bc9114ef67182e2b Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Fri, 12 Apr 2024 12:43:58 -0700
Subject: [PATCH 100/101] feedback
---
azure_functions_worker/bindings/meta.py | 5 ++---
azure_functions_worker/functions.py | 3 +--
2 files changed, 3 insertions(+), 5 deletions(-)
diff --git a/azure_functions_worker/bindings/meta.py b/azure_functions_worker/bindings/meta.py
index c1ff6e2ee..de3d69db6 100644
--- a/azure_functions_worker/bindings/meta.py
+++ b/azure_functions_worker/bindings/meta.py
@@ -11,15 +11,14 @@
from .shared_memory_data_transfer import SharedMemoryManager
from ..http_v2 import HttpV2Registry
-from ..constants import CUSTOMER_PACKAGES_PATH, HTTP, HTTP_TRIGGER
+from ..constants import CUSTOMER_PACKAGES_PATH, HTTP, HTTP_TRIGGER, \
+ BASE_EXT_SUPPORTED_PY_MINOR_VERSION
from ..logging import logger
PB_TYPE = 'rpc_data'
PB_TYPE_DATA = 'data'
PB_TYPE_RPC_SHARED_MEMORY = 'rpc_shared_memory'
-# Base extension supported Python minor version
-BASE_EXT_SUPPORTED_PY_MINOR_VERSION = 8
BINDING_REGISTRY = None
DEFERRED_BINDING_REGISTRY = None
diff --git a/azure_functions_worker/functions.py b/azure_functions_worker/functions.py
index 258a24ab9..cbfef029c 100644
--- a/azure_functions_worker/functions.py
+++ b/azure_functions_worker/functions.py
@@ -6,8 +6,7 @@
import typing
import uuid
-from azure_functions_worker.constants import HTTP_TRIGGER
-
+from .constants import HTTP_TRIGGER
from . import bindings as bindings_utils
from . import protos
from ._thirdparty import typing_inspect
From 87399e7fa3d5ffcbee20e19167db5ce419417e53 Mon Sep 17 00:00:00 2001
From: peterstone2017 <12449837+YunchuWang@users.noreply.github.com>
Date: Fri, 12 Apr 2024 12:47:43 -0700
Subject: [PATCH 101/101] feedback
---
.../fastapi/file_name/main.py | 43 -------------------
1 file changed, 43 deletions(-)
delete mode 100644 tests/endtoend/http_functions/http_functions_v2/fastapi/file_name/main.py
diff --git a/tests/endtoend/http_functions/http_functions_v2/fastapi/file_name/main.py b/tests/endtoend/http_functions/http_functions_v2/fastapi/file_name/main.py
deleted file mode 100644
index b149c1f78..000000000
--- a/tests/endtoend/http_functions/http_functions_v2/fastapi/file_name/main.py
+++ /dev/null
@@ -1,43 +0,0 @@
-from datetime import datetime
-import logging
-import time
-
-import azure.functions as func
-
-from azurefunctions.extensions.http.fastapi import Request, Response
-
-app = func.FunctionApp(http_auth_level=func.AuthLevel.ANONYMOUS)
-
-
-@app.route(route="default_template")
-async def default_template(req: Request) -> Response:
- logging.info('Python HTTP trigger function processed a request.')
-
- name = req.query_params.get('name')
- if not name:
- try:
- req_body = await req.json()
- except ValueError:
- pass
- else:
- name = req_body.get('name')
-
- if name:
- return Response(
- f"Hello, {name}. This HTTP triggered function "
- f"executed successfully.")
- else:
- return Response(
- "This HTTP triggered function executed successfully. "
- "Pass a name in the query string or in the request body for a"
- " personalized response.",
- status_code=200
- )
-
-
-@app.route(route="http_func")
-def http_func(req: Request) -> Response:
- time.sleep(1)
-
- current_time = datetime.now().strftime("%H:%M:%S")
- return Response(f"{current_time}")